feat(systems): a System names its files — path patterns stored, validated and matched (milestone 444 step 3, #4756)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m6s
CI & Build / Python tests (push) Successful in 1m50s
CI & Build / Build & push image (push) Successful in 43s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m6s
CI & Build / Python tests (push) Successful in 1m50s
CI & Build / Build & push image (push) Successful in 43s
A System gains path_patterns: globs relative to the repo root (* within one directory, ** across any depth, a plain directory covering everything under it). One service validates them for every door, so the web UI and MCP refuse the same bad pattern with the same message. systems_for_paths resolves paths to every active System that covers them, which step 4 (#4757) uses to deliver an area's rulings when its files are touched. - schema: systems.path_patterns JSONB NOT NULL default [] (migration 0113) - service: normalize_path_patterns, path_matches, systems_for_paths - routes + MCP create_system/update_system accept it; [] clears - web UI: a Files field in the create and edit forms, patterns on the card - backup carries it through export and restore - using-scribe reflex 7: tagging work keeps a System's files current Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ const newDescription = ref("");
|
||||
// feature two matchers to keep in step, which is the exact drift the catalog
|
||||
// exists to end. The server still applies an exact hit on submit.
|
||||
const newCanonicalId = ref<number | null>(null);
|
||||
const newPaths = ref("");
|
||||
const creating = ref(false);
|
||||
// An `overlap` the server offered after a create — an offer, never applied.
|
||||
const suggestion = ref<{ systemId: number; match: CanonicalMatch } | null>(null);
|
||||
@@ -37,6 +38,7 @@ const editingId = ref<number | null>(null);
|
||||
const editName = ref("");
|
||||
const editDescription = ref("");
|
||||
const editCanonicalId = ref<number | null>(null);
|
||||
const editPaths = ref("");
|
||||
const savingEdit = ref(false);
|
||||
|
||||
// Mapping review
|
||||
@@ -55,6 +57,16 @@ const visibleSystems = computed(() =>
|
||||
|
||||
const proposals = computed(() => canon.proposalsByProject[props.projectId] ?? []);
|
||||
|
||||
// The files field is one pattern per line. The server is the one that
|
||||
// validates and tidies them, so this only splits — a second validator here
|
||||
// would be a second answer to "is this pattern acceptable".
|
||||
function parsePaths(text: string): string[] {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function areaName(system: System): string | null {
|
||||
return canon.byId(system.canonical_id)?.name ?? null;
|
||||
}
|
||||
@@ -89,6 +101,7 @@ function openCreate() {
|
||||
newName.value = "";
|
||||
newDescription.value = "";
|
||||
newCanonicalId.value = null;
|
||||
newPaths.value = "";
|
||||
}
|
||||
|
||||
function cancelCreate() {
|
||||
@@ -96,6 +109,7 @@ function cancelCreate() {
|
||||
newName.value = "";
|
||||
newDescription.value = "";
|
||||
newCanonicalId.value = null;
|
||||
newPaths.value = "";
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
@@ -107,6 +121,7 @@ async function submitCreate() {
|
||||
name,
|
||||
description: newDescription.value.trim() || undefined,
|
||||
canonical_id: newCanonicalId.value ?? undefined,
|
||||
path_patterns: parsePaths(newPaths.value),
|
||||
});
|
||||
cancelCreate();
|
||||
if (created.canonical_suggestion) {
|
||||
@@ -157,6 +172,7 @@ function startEdit(system: System) {
|
||||
editName.value = system.name;
|
||||
editDescription.value = system.description;
|
||||
editCanonicalId.value = system.canonical_id;
|
||||
editPaths.value = system.path_patterns.join("\n");
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
@@ -171,6 +187,7 @@ async function submitEdit(system: System) {
|
||||
await store.updateSystem(props.projectId, system.id, {
|
||||
name,
|
||||
description: editDescription.value.trim(),
|
||||
path_patterns: parsePaths(editPaths.value),
|
||||
});
|
||||
// The mapping is a separate write with its own validation — one column,
|
||||
// one writer (services/canonical_systems.set_system_canonical).
|
||||
@@ -180,8 +197,9 @@ async function submitEdit(system: System) {
|
||||
}
|
||||
editingId.value = null;
|
||||
toast.show("System updated");
|
||||
} catch {
|
||||
toast.show("Failed to update system", "error");
|
||||
} catch (e) {
|
||||
// A refused pattern comes back as a 400 naming it — say which.
|
||||
toast.show(apiErrorMessage(e, "Failed to update system"), "error");
|
||||
} finally {
|
||||
savingEdit.value = false;
|
||||
}
|
||||
@@ -337,6 +355,20 @@ async function confirmDelete() {
|
||||
Files this system under an area shared by every project. Your name stays as you typed it.
|
||||
</span>
|
||||
</label>
|
||||
<label class="area-field">
|
||||
<span class="area-label">Files</span>
|
||||
<textarea
|
||||
v-model="newPaths"
|
||||
class="fs-input system-textarea system-paths-input"
|
||||
rows="2"
|
||||
placeholder="e.g. src/billing — one pattern per line"
|
||||
aria-label="System files"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<span class="field-hint">
|
||||
One pattern per line, from the repo root. <code>*</code> stays in one folder, <code>**</code> reaches any depth, and a folder covers everything in it.
|
||||
</span>
|
||||
</label>
|
||||
<div class="system-form-actions">
|
||||
<button type="submit" class="btn-primary btn-compact" :disabled="!newName.trim() || creating">
|
||||
{{ creating ? "Creating…" : "Create" }}
|
||||
@@ -397,6 +429,17 @@ async function confirmDelete() {
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="area-field">
|
||||
<span class="area-label">Files</span>
|
||||
<textarea
|
||||
v-model="editPaths"
|
||||
class="fs-input system-textarea system-paths-input"
|
||||
rows="2"
|
||||
placeholder="One pattern per line, from the repo root"
|
||||
aria-label="System files"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="system-form-actions">
|
||||
<button type="submit" class="btn-primary btn-compact" :disabled="!editName.trim() || savingEdit">
|
||||
{{ savingEdit ? "Saving…" : "Save" }}
|
||||
@@ -430,6 +473,9 @@ async function confirmDelete() {
|
||||
>{{ areaName(system) }}</span>
|
||||
</div>
|
||||
<p v-if="system.description" class="system-description">{{ system.description }}</p>
|
||||
<ul v-if="system.path_patterns.length" class="system-paths" aria-label="Files">
|
||||
<li v-for="pattern in system.path_patterns" :key="pattern" class="system-path">{{ pattern }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="system-actions">
|
||||
<button class="action-btn" title="Edit" aria-label="Edit system" @click="startEdit(system)">
|
||||
@@ -627,6 +673,28 @@ async function confirmDelete() {
|
||||
.system-textarea { resize: vertical; }
|
||||
|
||||
.system-form-actions { display: flex; gap: 0.4rem; }
|
||||
.system-paths-input { font-family: var(--fs-font-mono); font-size: 0.8rem; }
|
||||
|
||||
/* The area's files (milestone 444). Mono because they are paths to be read
|
||||
character for character; small because they qualify the card, not lead it. */
|
||||
.system-paths {
|
||||
list-style: none;
|
||||
margin: var(--fs-space-2) 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--fs-space-1);
|
||||
}
|
||||
.system-path {
|
||||
font-family: var(--fs-font-mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--fs-text-secondary);
|
||||
background: var(--fs-surface-page);
|
||||
border: 1px solid var(--fs-border-color);
|
||||
border-radius: var(--fs-radius-sm);
|
||||
padding: 0.05rem 0.4rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* RESTORED (#2444). Both lost their base rule to a CSS sweep; only the
|
||||
`--archived` modifier and the `:hover .system-actions` reveal survived.
|
||||
|
||||
Reference in New Issue
Block a user