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:
@@ -14,6 +14,12 @@ export interface System {
|
||||
color: string | null;
|
||||
status: "active" | "archived";
|
||||
order_index: number;
|
||||
/**
|
||||
* The files that are this area, as globs relative to the repo root: `*`
|
||||
* within one directory, `**` across any depth, a plain directory covering
|
||||
* everything under it. Empty means the area has not named its files.
|
||||
*/
|
||||
path_patterns: string[];
|
||||
open_issue_count: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
@@ -39,7 +45,13 @@ export interface CreatedSystem extends System {
|
||||
|
||||
export async function createSystem(
|
||||
projectId: number,
|
||||
data: { name: string; description?: string; color?: string; canonical_id?: number },
|
||||
data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
canonical_id?: number;
|
||||
path_patterns?: string[];
|
||||
},
|
||||
): Promise<CreatedSystem> {
|
||||
return apiPost(`/api/projects/${projectId}/systems`, data);
|
||||
}
|
||||
@@ -53,6 +65,7 @@ export async function updateSystem(
|
||||
color: string | null;
|
||||
status: "active" | "archived";
|
||||
order_index: number;
|
||||
path_patterns: string[];
|
||||
}>,
|
||||
): Promise<System> {
|
||||
return apiPatch(`/api/projects/${projectId}/systems/${systemId}`, data);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useSystemsStore = defineStore("systems", () => {
|
||||
|
||||
async function createSystem(
|
||||
projectId: number,
|
||||
data: { name: string; description?: string; color?: string; canonical_id?: number },
|
||||
data: Parameters<typeof api.createSystem>[1],
|
||||
) {
|
||||
const system = await api.createSystem(projectId, data);
|
||||
if (!systemsByProject.value[projectId]) systemsByProject.value[projectId] = [];
|
||||
@@ -33,7 +33,7 @@ export const useSystemsStore = defineStore("systems", () => {
|
||||
async function updateSystem(
|
||||
projectId: number,
|
||||
systemId: number,
|
||||
data: Partial<Pick<System, "name" | "description" | "color" | "status" | "order_index">>,
|
||||
data: Parameters<typeof api.updateSystem>[2],
|
||||
) {
|
||||
const system = await api.updateSystem(projectId, systemId, data);
|
||||
const list = systemsByProject.value[projectId];
|
||||
|
||||
Reference in New Issue
Block a user