@ybotok/loam 0.2.0-alpha.2 → 0.2.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +3 -3
- package/ROADMAP.md +37 -5
- package/SCHEMA.md +33 -1
- package/dist/commands/archive/run.js +46 -2
- package/dist/commands/archive/run.js.map +1 -1
- package/dist/commands/seed/plan.js +1 -1
- package/dist/commands/seed/plan.js.map +1 -1
- package/dist/commands/subsystem/sync.js +1 -1
- package/dist/commands/subsystem/sync.js.map +1 -1
- package/dist/commands/subsystem/txn/txn.js +1 -1
- package/dist/commands/subsystem/txn/txn.js.map +1 -1
- package/dist/commands/subsystem/txn/views.js +23 -11
- package/dist/commands/subsystem/txn/views.js.map +1 -1
- package/dist/commands/validate/fleet/views/ids.js +6 -1
- package/dist/commands/validate/fleet/views/ids.js.map +1 -1
- package/dist/commands/validate/fleet/views/stale.js +15 -39
- package/dist/commands/validate/fleet/views/stale.js.map +1 -1
- package/dist/core/agent/agents-md/map/subsystem.js +8 -1
- package/dist/core/agent/agents-md/map/subsystem.js.map +1 -1
- package/dist/core/agent/agents-md.js +4 -0
- package/dist/core/agent/agents-md.js.map +1 -1
- package/dist/core/repo/paths.js +1 -1
- package/dist/core/repo/paths.js.map +1 -1
- package/dist/core/repo/tree/render/body.js +219 -0
- package/dist/core/repo/tree/render/body.js.map +1 -0
- package/dist/core/repo/tree/render/stale.js +26 -0
- package/dist/core/repo/tree/render/stale.js.map +1 -0
- package/dist/core/repo/tree/render/text.js +84 -0
- package/dist/core/repo/tree/render/text.js.map +1 -0
- package/dist/core/repo/tree/{views.js → render/views.js} +38 -34
- package/dist/core/repo/tree/render/views.js.map +1 -0
- package/examples/docs/architecture/subsystems.likec4 +4 -0
- package/package.json +1 -1
- package/dist/core/repo/tree/views.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,IAAI,EAAkB,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAuB,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnG,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAAqB,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"txn.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/txn.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared commit window every subsystem WRITE verb goes through — `new`,\n * `rm`, `move` and `rename` all land here: lock, recover a predecessor's\n * journal, plan against the walked tree, regenerate the views file from the\n * PLANNED post-state, commit through the journaled transaction (directory\n * renames included), report. One window, because the views file must land in\n * the same commit as whatever changed the tree — a marker without its view,\n * or renames without theirs, is exactly the `subsystem.views-stale` state\n * the generated file's contract refuses to leave between two commits.\n */\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { fail, type ErrorCode } from \"../../../core/envelope/json.js\";\nimport { subsystemViewsPath } from \"../../../core/repo/paths.js\";\nimport { listFleetTree } from \"../../../core/repo/repo.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\nimport { stageWrites } from \"../../../core/staging/commit.js\";\nimport { type CommitRecovery, InterruptedCommitError } from \"../../../core/staging/interrupted.js\";\nimport { acquireDocsLock, DocsBusyError } from \"../../../core/staging/lock.js\";\nimport { recoverInterruptedCommit } from \"../../../core/staging/recovery/recover.js\";\nimport { commitStaged } from \"../../../core/staging/txn/transaction.js\";\nimport { type PlannedWrite } from \"../../../core/staging/writes.js\";\nimport { viewsAgree } from \"../../../core/repo/tree/render/views.js\";\nimport { expectedViews } from \"./views.js\";\n\nexport type ViewsAction = \"created\" | \"updated\" | \"removed\" | \"current\";\n\n/** The text tail a verb's success line carries about the generated file. */\nexport function reportViews(views: ViewsAction): string {\n return views === \"current\" ? \"\" : ` — architecture/subsystems.likec4 ${views}`;\n}\n\n/** What one subsystem verb asks the shared commit window to land. */\nexport interface SubsystemTxn {\n /** The journal's `target` — what this commit is FOR. */\n target: string;\n /** Marker writes (create/delete); the views write is derived and appended. */\n writes: PlannedWrite[];\n /** The tree AS THE COMMIT LEAVES IT — the views file is rendered from this. */\n tree: FleetTree;\n /** Directory renames, performed after the swaps, in this order. */\n moves?: { from: string; to: string }[];\n /** Remap of a RACED commit failure (new: a lost marker create is `already-exists`). */\n racedCode?: ErrorCode;\n /** Remap of a rolled-back commit failure (move: `move-failed` — no merge was computed). */\n failedCode?: ErrorCode;\n /** The rollback sentence's verb for half-state files. */\n what: string;\n report: (views: ViewsAction, recovered: CommitRecovery | null) => void | Promise<void>;\n}\n\n/**\n * Lock → recover → plan → stage (views included) → journaled commit → report.\n * `plan` returning null means it already reported a refusal; nothing was\n * staged and nothing is written.\n */\nexport async function commitWindow(\n docsDir: DocsDir,\n json: boolean,\n plan: (tree: FleetTree) => Promise<SubsystemTxn | null>,\n): Promise<void> {\n let releaseLock: () => Promise<void>;\n try {\n releaseLock = await acquireDocsLock(docsDir);\n } catch (err) {\n if (!(err instanceof DocsBusyError)) throw err;\n fail(json, \"docs-busy\", err.message);\n return;\n }\n try {\n let recovered: CommitRecovery | null;\n try {\n recovered = await recoverInterruptedCommit(docsDir);\n } catch (err) {\n if (!(err instanceof InterruptedCommitError)) throw err;\n fail(json, \"commit-interrupted\", err.message);\n return;\n }\n const txn = await plan(await listFleetTree(docsDir));\n if (txn === null) return;\n\n const path = subsystemViewsPath(docsDir);\n const expected = await expectedViews(docsDir, txn.tree);\n const bytes = expected === null ? null : Buffer.from(expected, \"utf8\");\n // Content, not bytes — `viewsAgree` carries the reason. Without it every\n // subsystem move on a Windows clone folded a rewrite of this file into its\n // transaction that changed not one fact, and journalled it.\n const current = existsSync(path) ? await readFile(path, \"utf8\") : null;\n let views: ViewsAction = \"current\";\n const writes = [...txn.writes];\n if (!viewsAgree(current, expected)) {\n views = current === null ? \"created\" : bytes === null ? \"removed\" : \"updated\";\n writes.push({ path, content: bytes });\n }\n const staged = await stageWrites(writes);\n const committed = await commitStaged(\n {\n root: docsDir,\n command: \"subsystem\",\n rerun: \"loam subsystem sync\",\n target: txn.target,\n ...(txn.moves === undefined ? {} : { moves: txn.moves }),\n },\n staged,\n txn.what,\n );\n if (!committed.ok) {\n const code =\n committed.code === \"rollback-incomplete\"\n ? committed.code\n : committed.raced\n ? (txn.racedCode ?? txn.failedCode ?? committed.code)\n : (txn.failedCode ?? committed.code);\n fail(json, code, committed.message);\n return;\n }\n await txn.report(views, recovered);\n } finally {\n await releaseLock();\n }\n}\n"]}
|
|
@@ -7,24 +7,36 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { relative, sep } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import {} from "../../../core/c4/likec4.js";
|
|
11
|
+
import { loadArchitecture } from "../../../core/c4/project/architecture.js";
|
|
11
12
|
import { serviceDirOf } from "../../../core/kernel/ids/dirs.js";
|
|
12
13
|
import { landscapePath } from "../../../core/repo/paths.js";
|
|
13
|
-
import { renderSubsystemViews } from "../../../core/repo/tree/views.js";
|
|
14
|
+
import { renderSubsystemViews } from "../../../core/repo/tree/render/views.js";
|
|
14
15
|
/**
|
|
15
|
-
* The
|
|
16
|
-
* map is absent or does not parse. Tolerant on
|
|
17
|
-
* runnable in a repo whose landscape is broken — it
|
|
18
|
-
* committed bytes resolve (no includes), deterministically,
|
|
19
|
-
* after the landscape is repaired catches the file up.
|
|
20
|
-
* staleness question in exactly those states
|
|
16
|
+
* The elements of the whole `architecture/` PROJECT for the member join, or
|
|
17
|
+
* the empty list when the map is absent or does not parse. Tolerant on
|
|
18
|
+
* purpose: sync must stay runnable in a repo whose landscape is broken — it
|
|
19
|
+
* renders what the committed bytes resolve (no includes), deterministically,
|
|
20
|
+
* and the next sync after the landscape is repaired catches the file up.
|
|
21
|
+
* `validate` skips the staleness question in exactly those states.
|
|
22
|
+
*
|
|
23
|
+
* The PROJECT, not the landscape file alone, and that is a correctness rule
|
|
24
|
+
* rather than a preference: `validate` grades this file against
|
|
25
|
+
* `loadArchitecture` — the landscape merged with every
|
|
26
|
+
* `architecture/usecases/*.likec4`, which may `extend` the model and bind
|
|
27
|
+
* elements of its own. While this side read the landscape alone, a use-case
|
|
28
|
+
* document declaring one element bound to a filed service was enough to make
|
|
29
|
+
* `sync` answer `current` while `validate --all` reported
|
|
30
|
+
* `subsystem.views-stale` and named `sync` as the repair — a loop no command
|
|
31
|
+
* could clear, reproduced on `examples/docs` before this call changed. Two
|
|
32
|
+
* readers of "what should the views say" must read the same documents, or the
|
|
33
|
+
* shared render in `core/repo/tree/render/views.ts` is shared in name only.
|
|
21
34
|
*/
|
|
22
35
|
export async function landscapeElements(docsDir) {
|
|
23
|
-
|
|
24
|
-
if (!existsSync(path))
|
|
36
|
+
if (!existsSync(landscapePath(docsDir)))
|
|
25
37
|
return [];
|
|
26
38
|
try {
|
|
27
|
-
const doc = await
|
|
39
|
+
const doc = await loadArchitecture(docsDir);
|
|
28
40
|
return doc.errors.length > 0 ? [] : doc.elements;
|
|
29
41
|
}
|
|
30
42
|
catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../../../../src/commands/subsystem/txn/views.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAa,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAgB,MAAM,kCAAkC,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAG/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAAgB;IACtD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACnD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC5C,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,IAAe;IACnE,OAAO,oBAAoB,CAAC,IAAI,EAAE,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,IAAe,EAAE,OAAuC,EAAE,YAAoB;IACtG,MAAM,KAAK,GAAG,CAAC,GAAW,EAAU,EAAE;QACpC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC;gBAAE,OAAO,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7F,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,CAAC,GAAW,EAAY,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClF,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAChC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACzB,iEAAiE;YACjE,gEAAgE;YAChE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,CAAC,CAAC;QACF,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACpC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3C,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What the generated views file MUST say — the shared answer every subsystem\n * writer and `subsystem sync` render from. A module beneath the verbs (in the\n * txn/ subpackage) because the commit window and sync both consume it, and a\n * second spelling of \"what should the views say\" would be a second chance to\n * disagree with `validate`'s staleness check.\n */\nimport { existsSync } from \"node:fs\";\nimport { relative, sep } from \"node:path\";\nimport { type Elem } from \"../../../core/c4/likec4.js\";\nimport { loadArchitecture } from \"../../../core/c4/project/architecture.js\";\nimport { serviceDirOf, type DocsDir } from \"../../../core/kernel/ids/dirs.js\";\nimport { landscapePath } from \"../../../core/repo/paths.js\";\nimport { renderSubsystemViews } from \"../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../core/repo/tree/walk.js\";\n\n/**\n * The elements of the whole `architecture/` PROJECT for the member join, or\n * the empty list when the map is absent or does not parse. Tolerant on\n * purpose: sync must stay runnable in a repo whose landscape is broken — it\n * renders what the committed bytes resolve (no includes), deterministically,\n * and the next sync after the landscape is repaired catches the file up.\n * `validate` skips the staleness question in exactly those states.\n *\n * The PROJECT, not the landscape file alone, and that is a correctness rule\n * rather than a preference: `validate` grades this file against\n * `loadArchitecture` — the landscape merged with every\n * `architecture/usecases/*.likec4`, which may `extend` the model and bind\n * elements of its own. While this side read the landscape alone, a use-case\n * document declaring one element bound to a filed service was enough to make\n * `sync` answer `current` while `validate --all` reported\n * `subsystem.views-stale` and named `sync` as the repair — a loop no command\n * could clear, reproduced on `examples/docs` before this call changed. Two\n * readers of \"what should the views say\" must read the same documents, or the\n * shared render in `core/repo/tree/render/views.ts` is shared in name only.\n */\nexport async function landscapeElements(docsDir: DocsDir): Promise<Elem[]> {\n if (!existsSync(landscapePath(docsDir))) return [];\n try {\n const doc = await loadArchitecture(docsDir);\n return doc.errors.length > 0 ? [] : doc.elements;\n } catch {\n return [];\n }\n}\n\n/** The bytes the views file must hold for this tree, or null for \"must be absent\". */\nexport async function expectedViews(docsDir: DocsDir, tree: FleetTree): Promise<string | null> {\n return renderSubsystemViews(tree, await landscapeElements(docsDir));\n}\n\n/**\n * The tree AS THE RENAMES LEAVE IT, computed in memory: every dir under a\n * moved root is remapped by prefix, and each entry's placement chain is\n * re-derived from its remapped dir. The views file is rendered from this, so\n * the commit lands file and renames agreeing — never from a re-walk that\n * would have to happen between two states. Beside `expectedViews` because the\n * two are one promise: what the commit renames and what the file says must\n * come from the same in-memory answer.\n */\nexport function movedTree(tree: FleetTree, renames: { from: string; to: string }[], servicesRoot: string): FleetTree {\n const remap = (dir: string): string => {\n for (const r of renames) {\n if (dir === r.from || dir.startsWith(r.from + sep)) return r.to + dir.slice(r.from.length);\n }\n return dir;\n };\n const chainOf = (dir: string): string[] => relative(servicesRoot, dir).split(sep);\n return {\n findings: tree.findings,\n services: tree.services.map((s) => {\n const dir = remap(s.dir);\n // The constructor call records provenance: a readdir-established\n // directory carried through a rename this same commit performs.\n return { ...s, dir: serviceDirOf(dir), subsystem: chainOf(dir).slice(0, -1) };\n }),\n subsystems: tree.subsystems.map((s) => {\n const dir = remap(s.dir);\n return { ...s, dir, path: chainOf(dir) };\n }),\n };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from "../../../../core/repo/tree/views.js";
|
|
1
|
+
import { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from "../../../../core/repo/tree/render/views.js";
|
|
2
2
|
/**
|
|
3
3
|
* `authored` is every view id the `architecture/` project claims, each with the
|
|
4
4
|
* file that claims it — static views included, because a static view claims an
|
|
@@ -28,6 +28,11 @@ export function viewIdFindings(authored, tree) {
|
|
|
28
28
|
severity: "error",
|
|
29
29
|
code: "subsystem.view-id-collision",
|
|
30
30
|
subject: id,
|
|
31
|
+
// The CLAIMANT file — the one the message tells the reader to edit, and
|
|
32
|
+
// the one the report's target fallback gets wrong: it files this under
|
|
33
|
+
// the landscape, which is exactly the file a reader must NOT rename the
|
|
34
|
+
// view in when the claim came from a document of their own.
|
|
35
|
+
locations: [{ path: `architecture/${file}`, role: "primary" }],
|
|
31
36
|
message: `subsystems: architecture/${file} declares \`view ${id}\`, which is the id loam generates ` +
|
|
32
37
|
`into architecture/subsystems.likec4 for services/${owner}/ — LikeC4 merges both files into one project, ` +
|
|
33
38
|
`so it refuses the whole architecture/ project and renders nothing. ` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/ids.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/ids.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAC;AAIpG;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAmC,EAAE,IAAe;IACjF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,8EAA8E;IAC9E,+EAA+E;IAC/E,2EAA2E;IAC3E,iDAAiD;IACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,QAAQ;QAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IACtG,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,EAAE;YACX,wEAAwE;YACxE,uEAAuE;YACvE,wEAAwE;YACxE,4DAA4D;YAC5D,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,gBAAgB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAkB,EAAE,CAAC;YACvE,OAAO,EACL,4BAA4B,IAAI,oBAAoB,EAAE,qCAAqC;gBAC3F,oDAAoD,KAAK,iDAAiD;gBAC1G,qEAAqE;gBACrE,mCAAmC,IAAI,qBAAqB,qBAAqB,gBAAgB;gBACjG,iHAAiH;SACpH,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * An authored view id that collides with one loam GENERATES into\n * `architecture/subsystems.likec4`.\n *\n * This closes a green-while-broken hole, and the shape of it is measured rather\n * than argued. The LikeC4 renderer merges every `.likec4` file in a project into\n * one model, and `likec4.config.json` scopes the root project to\n * `architecture/` — so `landscape.likec4` and the generated `subsystems.likec4`\n * share ONE flat view-id namespace. Author `view subsystem_commerce { }` in the\n * landscape while the tree has a `commerce` subsystem and the renderer refuses\n * the ENTIRE `architecture/` project with two `Duplicate view` diagnostics, one\n * per file — while `loam validate --all` printed `landscape: N service(s)\n * modelled — architecture/landscape.likec4 and services/ agree`. Reproduced at\n * the 1.59.2 pin.\n *\n * Nothing else in a loam repo can produce this. A duplicate INSIDE one document\n * is already 2 LikeC4 errors, so `landscape.invalid` has it (measured). A\n * service's `model.likec4` and a feature's `delta.likec4` are each their own\n * LikeC4 project — `likec4.config.json` excludes `services/**` and\n * `features/**` from the root — so their view ids cannot collide with the\n * landscape's, and a delta's views never travel through `archive` anyway. That\n * leaves exactly one pair, which is why this check compares exactly one pair\n * rather than taking a census across every document loam parses.\n *\n * ONLY the real collision is refused, never the merely reserved-looking name.\n * A landscape view called `subsystem_overview` with no `overview` subsystem\n * breaks nothing today, and refusing it would be loam inventing a rule about a\n * name that harms no one. The latent case is not lost: the moment somebody\n * creates that subsystem, the generator mints the id, and this same check fires\n * on the same run. The message names the reservation anyway, so an author fixing\n * one collision learns not to make the next.\n *\n * Graded on `validate --all` only, and the caller holds that gate together with\n * the landscape-parsed one: under a single-target run the fleet tree is not\n * walked and the generated ids are unknown, so the question is unanswerable and\n * the check stays silent rather than half-answering.\n */\nimport type { ViewIdClaim } from \"../../../../core/c4/parsed/view-ids.js\";\nimport { SUBSYSTEM_VIEW_PREFIX, subsystemViewId } from \"../../../../core/repo/tree/render/views.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\n/**\n * `authored` is every view id the `architecture/` project claims, each with the\n * file that claims it — static views included, because a static view claims an\n * id exactly as firmly as a dynamic one. Absent (the document did not parse, or\n * an older loader did not read them) is treated as \"nothing to compare\", never\n * as \"nothing collides\".\n */\nexport function viewIdFindings(authored: ViewIdClaim[] | undefined, tree: FleetTree): Finding[] {\n if (authored === undefined || authored.length === 0) return [];\n const generated = new Map(tree.subsystems.map((sub) => [subsystemViewId(sub), sub.path.join(\"/\")]));\n const findings: Finding[] = [];\n // Sorted and de-duplicated by id so a project claiming the same id twice —\n // itself already a parse error, but reachable through a partial read — yields\n // one finding per id rather than one per occurrence. The first claimant's file\n // is the one named: with a duplicate the project does not parse at all, so\n // there is never a second one to choose between.\n const claimants = new Map<string, string>();\n for (const claim of authored) if (!claimants.has(claim.id)) claimants.set(claim.id, claim.sourcePath);\n for (const [id, file] of [...claimants].sort(([a], [b]) => (a < b ? -1 : 1))) {\n const owner = generated.get(id);\n if (owner === undefined) continue;\n findings.push({\n severity: \"error\",\n code: \"subsystem.view-id-collision\",\n subject: id,\n // The CLAIMANT file — the one the message tells the reader to edit, and\n // the one the report's target fallback gets wrong: it files this under\n // the landscape, which is exactly the file a reader must NOT rename the\n // view in when the claim came from a document of their own.\n locations: [{ path: `architecture/${file}`, role: \"primary\" as const }],\n message:\n `subsystems: architecture/${file} declares \\`view ${id}\\`, which is the id loam generates ` +\n `into architecture/subsystems.likec4 for services/${owner}/ — LikeC4 merges both files into one project, ` +\n `so it refuses the whole architecture/ project and renders nothing. ` +\n `Rename the view in architecture/${file}; ids beginning \\`${SUBSYSTEM_VIEW_PREFIX}\\` are loam's ` +\n \"to mint, and architecture/subsystems.likec4 is generated (`loam subsystem sync`) and must never be hand-edited.\",\n });\n }\n return findings;\n}\n"]}
|
|
@@ -1,44 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Staleness of the GENERATED `architecture/subsystems.likec4` against the
|
|
3
|
-
* tree: exactly ONE error on exactly one file, repaired by exactly one
|
|
4
|
-
* command (`loam subsystem sync`).
|
|
5
|
-
*
|
|
6
|
-
* A content compare, deliberately — never a parse. A views-only document does
|
|
7
|
-
* not parse standalone (its `include` lines reference elements the landscape
|
|
8
|
-
* defines; the spike in `core/repo/tree/views.ts` records it), and no check
|
|
9
|
-
* anywhere reads the file's MEANING: the file is a scoping convenience for
|
|
10
|
-
* the LikeC4 renderer, so the only question loam owes it is "is this what the
|
|
11
|
-
* tree renders to". Rule 26 leaves this untouched: what loam may
|
|
12
|
-
* read is a `dynamic view`'s declared steps out of a document it already
|
|
13
|
-
* parses — never a static view's `include` predicates, and never this
|
|
14
|
-
* generated file at all. Absent counts as a state of its own: a fleet
|
|
15
|
-
* with subsystems and no file is stale, and a fleet with NO subsystems and a
|
|
16
|
-
* leftover file is stale too — the render contract says the file must then
|
|
17
|
-
* not exist, or a group deleted months ago keeps a view forever.
|
|
18
|
-
*
|
|
19
|
-
* The compare is over CONTENT, not BYTES, and `viewsAgree` in
|
|
20
|
-
* `core/repo/tree/views.ts` both performs it and records why: an ordinary
|
|
21
|
-
* Windows clone hands this file back with CRLF and not one fact in it changed,
|
|
22
|
-
* and under a byte compare the advertised repair is a loop rather than a
|
|
23
|
-
* diagnosis. The rule lives beside the generator because the module that mints
|
|
24
|
-
* the bytes is the one that owes an answer about them.
|
|
25
|
-
*
|
|
26
|
-
* Graded only when the landscape PARSED (the caller holds that gate): the
|
|
27
|
-
* expected bytes resolve members through the landscape's element→service
|
|
28
|
-
* join, and grading them against a map that is missing or unreadable would
|
|
29
|
-
* cascade a second error behind `landscape.missing`/`landscape.invalid`,
|
|
30
|
-
* pointing at a file whose repair command would only re-derive the same
|
|
31
|
-
* incomplete answer.
|
|
32
|
-
*/
|
|
33
|
-
import { existsSync } from "node:fs";
|
|
34
|
-
import { readFile } from "node:fs/promises";
|
|
35
1
|
import { subsystemViewsPath } from "../../../../core/repo/paths.js";
|
|
36
|
-
import {
|
|
2
|
+
import { viewsState } from "../../../../core/repo/tree/render/stale.js";
|
|
37
3
|
export async function viewsStaleFindings(docsDir, tree, elements) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const actual = existsSync(path) ? await readFile(path, "utf8") : null;
|
|
41
|
-
if (viewsAgree(actual, expected))
|
|
4
|
+
const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree, elements);
|
|
5
|
+
if (agrees)
|
|
42
6
|
return [];
|
|
43
7
|
const state = actual === null
|
|
44
8
|
? "does not exist, but the tree has subsystems to mirror"
|
|
@@ -50,6 +14,18 @@ export async function viewsStaleFindings(docsDir, tree, elements) {
|
|
|
50
14
|
severity: "error",
|
|
51
15
|
code: "subsystem.views-stale",
|
|
52
16
|
subject: "architecture/subsystems.likec4",
|
|
17
|
+
// The file the message names, so an editor jumping to the finding lands
|
|
18
|
+
// on it. Without a location the report's fallback files this under the
|
|
19
|
+
// TARGET, which is the landscape — a reader was sent to
|
|
20
|
+
// architecture/landscape.likec4 to fix a file the sentence beside it
|
|
21
|
+
// named as architecture/subsystems.likec4. A file that does not exist
|
|
22
|
+
// yet cannot be `primary`: the actionable place is then the directory
|
|
23
|
+
// the regenerator will write into.
|
|
24
|
+
locations: [
|
|
25
|
+
actual === null
|
|
26
|
+
? { path: "architecture", role: "scope" }
|
|
27
|
+
: { path: "architecture/subsystems.likec4", role: "primary" },
|
|
28
|
+
],
|
|
53
29
|
message: `subsystems: architecture/subsystems.likec4 ${state} — ` +
|
|
54
30
|
`it is generated, one view per subsystem, and every derived scope drawn from it is wrong until it agrees. ` +
|
|
55
31
|
"Run `loam subsystem sync` to regenerate it; never edit it by hand.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/stale.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stale.js","sourceRoot":"","sources":["../../../../../src/commands/validate/fleet/views/stale.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAC;AAIxE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAgB,EAChB,IAAe,EACf,QAAgB;IAEhB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACnG,IAAI,MAAM;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,KAAK,GACT,MAAM,KAAK,IAAI;QACb,CAAC,CAAC,uDAAuD;QACzD,CAAC,CAAC,QAAQ,KAAK,IAAI;YACjB,CAAC,CAAC,4EAA4E;YAC9E,CAAC,CAAC,yBAAyB,CAAC;IAClC,OAAO;QACL;YACE,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,gCAAgC;YACzC,wEAAwE;YACxE,uEAAuE;YACvE,wDAAwD;YACxD,qEAAqE;YACrE,sEAAsE;YACtE,sEAAsE;YACtE,mCAAmC;YACnC,SAAS,EAAE;gBACT,MAAM,KAAK,IAAI;oBACb,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,OAAgB,EAAE;oBAClD,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,SAAkB,EAAE;aACzE;YACD,OAAO,EACL,8CAA8C,KAAK,KAAK;gBACxD,2GAA2G;gBAC3G,oEAAoE;SACvE;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Staleness of the GENERATED `architecture/subsystems.likec4` against the\n * tree: exactly ONE error on exactly one file, repaired by exactly one\n * command (`loam subsystem sync`).\n *\n * A content compare, deliberately — never a parse. A views-only document does\n * not parse standalone (its `include` lines reference elements the landscape\n * defines; the spike in `core/repo/tree/render/views.ts` records it), and no check\n * anywhere reads the file's MEANING: the file is a scoping convenience for\n * the LikeC4 renderer, so the only question loam owes it is \"is this what the\n * tree renders to\". Rule 26 leaves this untouched: what loam may\n * read is a `dynamic view`'s declared steps out of a document it already\n * parses — never a static view's `include` predicates, and never this\n * generated file at all. Absent counts as a state of its own: a fleet\n * with subsystems and no file is stale, and a fleet with NO subsystems and a\n * leftover file is stale too — the render contract says the file must then\n * not exist, or a group deleted months ago keeps a view forever.\n *\n * The compare is over CONTENT, not BYTES, and `viewsAgree` in\n * `core/repo/tree/render/views.ts` both performs it and records why: an ordinary\n * Windows clone hands this file back with CRLF and not one fact in it changed,\n * and under a byte compare the advertised repair is a loop rather than a\n * diagnosis. The rule lives beside the generator because the module that mints\n * the bytes is the one that owes an answer about them.\n *\n * Graded only when the landscape PARSED (the caller holds that gate): the\n * expected bytes resolve members through the landscape's element→service\n * join, and grading them against a map that is missing or unreadable would\n * cascade a second error behind `landscape.missing`/`landscape.invalid`,\n * pointing at a file whose repair command would only re-derive the same\n * incomplete answer.\n */\nimport type { Elem } from \"../../../../core/c4/likec4.js\";\nimport type { DocsDir } from \"../../../../core/kernel/ids/dirs.js\";\nimport { subsystemViewsPath } from \"../../../../core/repo/paths.js\";\nimport { viewsState } from \"../../../../core/repo/tree/render/stale.js\";\nimport type { FleetTree } from \"../../../../core/repo/tree/walk.js\";\nimport type { Finding } from \"../../../../core/vocabulary/report.js\";\n\nexport async function viewsStaleFindings(\n docsDir: DocsDir,\n tree: FleetTree,\n elements: Elem[],\n): Promise<Finding[]> {\n const { actual, expected, agrees } = await viewsState(subsystemViewsPath(docsDir), tree, elements);\n if (agrees) return [];\n const state =\n actual === null\n ? \"does not exist, but the tree has subsystems to mirror\"\n : expected === null\n ? \"exists, but the tree has no subsystems — the generated file must be absent\"\n : \"does not match the tree\";\n return [\n {\n severity: \"error\",\n code: \"subsystem.views-stale\",\n subject: \"architecture/subsystems.likec4\",\n // The file the message names, so an editor jumping to the finding lands\n // on it. Without a location the report's fallback files this under the\n // TARGET, which is the landscape — a reader was sent to\n // architecture/landscape.likec4 to fix a file the sentence beside it\n // named as architecture/subsystems.likec4. A file that does not exist\n // yet cannot be `primary`: the actionable place is then the directory\n // the regenerator will write into.\n locations: [\n actual === null\n ? { path: \"architecture\", role: \"scope\" as const }\n : { path: \"architecture/subsystems.likec4\", role: \"primary\" as const },\n ],\n message:\n `subsystems: architecture/subsystems.likec4 ${state} — ` +\n `it is generated, one view per subsystem, and every derived scope drawn from it is wrong until it agrees. ` +\n \"Run `loam subsystem sync` to regenerate it; never edit it by hand.\",\n },\n ];\n}\n"]}
|
|
@@ -26,7 +26,14 @@ export const SUBSYSTEM_COMMANDS = `- \`loam subsystem <verb>\` manages the group
|
|
|
26
26
|
moved and answers nothing — exit 0, no finding — when git will not say;
|
|
27
27
|
\`sync\` regenerates \`architecture/subsystems.likec4\`, the one repair for
|
|
28
28
|
\`subsystem.views-stale\`; a landscape view claiming a generated id is
|
|
29
|
-
\`subsystem.view-id-collision\`.
|
|
29
|
+
\`subsystem.view-id-collision\`. Each generated view is titled by its PATH —
|
|
30
|
+
every marked ancestor's label and its own, which the renderer reads as
|
|
31
|
+
folders — where a label is the marker's \`title\` or the directory name; the
|
|
32
|
+
marker's \`description\` rides along. So editing \`subsystem.yaml\` changes
|
|
33
|
+
what the renderer LABELS the group, a parent's title changes every view
|
|
34
|
+
beneath it, and either makes the file stale until the next \`sync\`. It is generated wholesale:
|
|
35
|
+
an authored diagram of the same services belongs in the landscape or in a
|
|
36
|
+
file of your own name, never here. \`loam adopt --subsystem <name>\` briefs a new
|
|
30
37
|
service's baseline directly into a group, so adoption need not land unfiled.
|
|
31
38
|
`;
|
|
32
39
|
//# sourceMappingURL=subsystem.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subsystem.js","sourceRoot":"","sources":["../../../../../src/core/agent/agents-md/map/subsystem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG
|
|
1
|
+
{"version":3,"file":"subsystem.js","sourceRoot":"","sources":["../../../../../src/core/agent/agents-md/map/subsystem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BjC,CAAC","sourcesContent":["/**\n * The `loam subsystem` half of the command map — its own section module\n * because command-map.ts sat against the file-line limit and the agents-md\n * package against the five-file cap, so the tree surface's documentation\n * lives one package down and is concatenated after the map.\n *\n * Same assembly contract as every section: ../agents-md.ts concatenates with\n * NO join separator, so this string starts at the first character of its\n * opening line and ends with the newline that closes its last one.\n */\nexport const SUBSYSTEM_COMMANDS = `- \\`loam subsystem <verb>\\` manages the grouping tree under \\`services/\\` — the\n navigable tree no identity depends on: a service id stays its leaf directory\n name, every join key is byte-identical before and after a move, and the tree\n carries no policy. Verbs: \\`new <name> [--under <sub>] [--title] [--description]\n [--owner]\\` creates a group (an empty one is legal — its \\`subsystem.yaml\\` is a\n marker, never a member list); \\`move <name>... --into <sub|.>\\` relocates\n services and whole subtrees in ONE journaled transaction over the renames\n plus the regenerated views file (\\`--into .\\` unfiles; the renames and the\n regenerated views file are staged in git without committing, where git answers); \\`rename <old> <new>\\` renames a\n group through the same transaction (services are never renamed — the id is\n the identity); \\`rm <name>\\` removes an EMPTY group; \\`list\\` shows the tree\n with member counts and the unfiled count (unfiled is permanent and normal —\n a count, never a finding; \\`loam list --json\\` carries the same facts as\n additive keys \\`services[].subsystem\\`, \\`subsystems[]\\` and\n \\`unfiledServices\\`); \\`history <name>\\` asks git how a service or subsystem\n moved and answers nothing — exit 0, no finding — when git will not say;\n \\`sync\\` regenerates \\`architecture/subsystems.likec4\\`, the one repair for\n \\`subsystem.views-stale\\`; a landscape view claiming a generated id is\n \\`subsystem.view-id-collision\\`. Each generated view is titled by its PATH —\n every marked ancestor's label and its own, which the renderer reads as\n folders — where a label is the marker's \\`title\\` or the directory name; the\n marker's \\`description\\` rides along. So editing \\`subsystem.yaml\\` changes\n what the renderer LABELS the group, a parent's title changes every view\n beneath it, and either makes the file stale until the next \\`sync\\`. It is generated wholesale:\n an authored diagram of the same services belongs in the landscape or in a\n file of your own name, never here. \\`loam adopt --subsystem <name>\\` briefs a new\n service's baseline directly into a group, so adoption need not land unfiled.\n`;\n"]}
|
|
@@ -18,6 +18,10 @@ loam has no server or database. Delete it and the documents still make sense.
|
|
|
18
18
|
|
|
19
19
|
\`architecture/landscape.likec4\` is the fleet map, and
|
|
20
20
|
\`architecture/adrs/\` optional system-level decisions — only their links are graded.
|
|
21
|
+
\`architecture/subsystems.likec4\` is GENERATED by \`loam subsystem sync\` from the
|
|
22
|
+
directory tree under \`services/\` — one view per subsystem, a scoping aid, never
|
|
23
|
+
hand-edited and never a second fleet map. A diagram you author belongs in the
|
|
24
|
+
landscape or in a file of your own name beside it.
|
|
21
25
|
\`services/<id>/\` holds a boundary's living \`model.likec4\`, \`spec.md\`,
|
|
22
26
|
\`arch.spec.md\`, OpenAPI, AsyncAPI, runbook and health facts.
|
|
23
27
|
\`features/<FEAT>/\` holds a change in flight: \`intent.md\`, \`delta.likec4\`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents-md.js","sourceRoot":"","sources":["../../../src/core/agent/agents-md.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEtD,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6JnB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,eAAe,CAAC,YAAY,CAAC,KAAK,WAAW,EAAE,CAAC","sourcesContent":["/**\n * The small, always-loaded orientation contract for a loam docs repository.\n *\n * Detailed grammars and workflows live behind `loam instructions`: those pages\n * describe the binary that is about to run, while this file is written once and\n * may be loaded into every agent turn for years. Keep only enough here to form\n * the next question, find the relevant page, and avoid an unsafe write.\n */\nimport { agentsStampLine } from \"./agents-stamp.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\n\nconst ORIENTATION = `# Working in this docs repo\n\nThis is a **loam** docs repo: plain files are the shared source of truth for a governed software\nfleet's architecture, requirements, contracts and verification evidence. A service may be a modular monolith, a network service, a CLI or a worker.\nloam has no server or database. Delete it and the documents still make sense.\n\n## What is where\n\n\\`architecture/landscape.likec4\\` is the fleet map, and\n\\`architecture/adrs/\\` optional system-level decisions — only their links are graded.\n\\`services/<id>/\\` holds a boundary's living \\`model.likec4\\`, \\`spec.md\\`,\n\\`arch.spec.md\\`, OpenAPI, AsyncAPI, runbook and health facts.\n\\`features/<FEAT>/\\` holds a change in flight: \\`intent.md\\`, \\`delta.likec4\\`,\nper-service requirement and contract deltas, and \\`verification.yaml\\`.\n\\`features/archive/\\` is shipped history.\n\nLiving documents are the complete current state. A feature document is a diff\nagainst them, reviewed as a diff; \\`loam archive\\` folds it into living state.\nDo not hand-edit the living landscape to add a feature change.\n\n## Linking between documents\n\nDocuments join with **standard markdown links** whose targets are real relative\npaths, for example \\`[decision](../adrs/ADR-001.md)\\`. They render in pull-request\nreview instead of showing literal brackets,\nand make “does this link resolve” a filesystem question; \\`[[Wikilinks]]\\` would\nrequire shortest-unique-path guessing.\nObsidian supports markdown links, autocomplete and rename-tracking when its\nwikilink setting is off. \\`link.unresolved\\` grades missing targets and CASE\n**is** graded; links outside this repository, \\`#section\\` targets, a fenced code\nblock or an inline code span are deliberately not graded.\n\n## \\`loam.json\\` — the wiring\n\nEvery repository commits its own \\`loam.json\\`:\n\n\\`{ \"docsDir\": \"../docs\", \"service\": \"payment-service\", \"gherkinDir\": \"features\" }\\`\n\n\\`docsDir\\` is stored exactly as it was passed and resolved against the directory\nholding the \\`loam.json\\`, so a relative path survives a clone; an absolute one\nraises \\`doctor.docs-absolute\\`. \\`service\\` binds a service checkout to the one boundary\nit may write evidence for. \\`gherkinDir\\` is optional. \\`agentTools\\` and\n\\`agentProfile\\` record which agent surfaces and workflow subset init manages;\n\\`agentFiles\\` records digests so unchanged generated pointers can refresh while\ncustomized ones remain untouched. With no config, loam refuses with \\`no-config\\`\nrather than guessing.\n\n\\`loam init --docs <dir>\\` **joins** an existing docs repo. Add \\`--create\\` only\nwhen creating a new one; a typo must not scaffold a second source of truth.\n\n## Starting from chat\n\nThere are two equivalent entry points into the same version-matched protocol.\nPrefer the explicit one when a user wants a predictable lifecycle: invoke\n\\`/loam-feature <FEAT> \"<title>\"\\` to propose, \\`/loam-implement <FEAT> [service]\\`\nto build, \\`/loam-check\\` and \\`/loam-verify <FEAT>\\` to grade it, and\n\\`/loam-ship <FEAT>\\` to finish. Most hosts use that flat slash\nspelling; Gemini uses \\`/loam:feature\\`, Amazon Q uses \\`@loam-feature\\`, and a\nskills-only host can invoke \\`$loam-feature\\`. Natural-language requests may load\nthe matching Agent Skill instead. Once either entry point is selected, the agent\ndrives the internal status/edit/validate loop and asks the user only for a real\ndecision or human-only act; the user does not have to type every loam command.\n\nIf loam or an agent integration behaves unexpectedly, invoke \\`/loam-report\\` (or\nload \\`$loam-report\\` on a skills-only host). It collects a sanitized, reproducible\nMarkdown report under \\`loam-reports/\\` without repairing the repository, retrying a\nwriter or uploading anything. This is support, not a seventh lifecycle step.\n\n## The cycle\n\n0. **Wire the repo** — in each service checkout run\n \\`loam init --docs <path-to-docs-repo> --service <id>\\`, then \\`loam doctor\\`.\n1. **Adopt** — for an undocumented boundary load \\`loam instructions loam-adopt\n <id>\\`, then use \\`loam adopt --service <id> --json\\`. Write drafts; a person,\n never an agent, later vouches for them.\n2. **Understand** — start or resume with \\`loam status --json\\`; its ordered\n \\`next[]\\` is the work queue. Read \\`next[0].execution\\`: run it only when\n \\`runnable\\` is true; otherwise use \\`kind\\`, \\`cwd\\`, \\`needs\\` and \\`after\\`\n to complete the edit or external-repo step. Use \\`loam list --json\\`, \\`loam show <id> --json\\`\n and \\`loam explore <id> --json\\` before proposing a change. Never propose a\n change to a service you have not read.\n3. **Scaffold** — load \\`loam instructions loam-feature <FEAT>\\`, then run\n \\`loam new <FEAT> --title \"...\" --touches <service>\\`.\n4. **Author** — fill the scaffolded TODOs and run \\`loam rebase <FEAT>\\` after\n reading the living baselines. The exact joins and authoring grammars are in\n the reference pages below.\n5. **Check** — load \\`loam instructions loam-check --no-fix-tables\\`, run\n \\`loam validate <FEAT> --json\\`, and use \\`loam explain <code>\\` for reported\n codes. \\`loam dependencies --json\\` names features that must archive first.\n6. **Build** — load \\`loam instructions loam-implement <FEAT>\\`.\n \\`loam delta <FEAT> --service <id> --json\\` is one service's task; in that\n service checkout, \\`loam gherkin <FEAT>\\` emits its executable scenarios.\n7. **Verify** — \\`loam verify <FEAT> --json\\` is the checklist. Record evidence\n from each affected service's own checkout. Read\n \\`loam instructions loam-done-check\\` before recording anything.\n8. **Ship** — load \\`loam instructions loam-ship <FEAT>\\`; begin with\n \\`loam archive <FEAT> --dry-run --json\\`, then archive only after the code is\n merged and the plan is the one reviewed.\n\n## Reading loam's output\n\nEvery command takes \\`--json\\`. The envelope has \\`contractVersion\\`, \\`ok\\` and,\non refusal, \\`error.code\\`. Unknown flags are \\`invalid-option\\`; \\`--help\\` and\n\\`--version\\` pass through. Branch on stable codes, never message prose.\n\nValidation \\`findings[]\\` carry \\`severity\\`; archive-related findings also carry\n\\`gates\\`. Severity and gating are two different questions: errors invalidate,\nwhile \\`gates: true\\` makes archive refuse — archive is gated on coherence, so a\ngating finding stops it whatever its severity. Advisory warnings never block.\n\\`--approve\\` overrides the gating issues — only those that are judgement calls —\nand never a mechanical loss. For example \\`delta.requirement-not-merged\\` warns but gates;\n\\`living.requirement-outside-requirements\\` cannot be approved, while\n\\`openapi.op-modified\\` is reviewable.\n\n\\`loam status --json\\` orders executable next actions. Findings include structured\n\\`locations[]\\`; use their paths instead of extracting filenames from prose.\n\\`loam show <FEAT> --json\\` includes a \\`review\\` pack with intent, architecture,\ncontracts, dependencies, verification, blockers and the same next actions.\n\\`loam explain <code> --json\\`\nreturns one code's meaning and fix; \\`loam explain --codes --json\\` lists them.\nWhich codes an INVOCATION can raise is in \\`loam instructions loam-codes\\`.\n\n## The identity rule\n\nA service is the element whose \\`metadata { service '<id>' }\\` matches its\n\\`services/<id>/\\` directory. An exact element id is the legacy fallback. Never\ninfer identity from display names or containment. Relationships join to contracts\nthrough exact ids: OpenAPI \\`operationId\\`, requirement \\`Operations:\\`, and C4\n\\`metadata { op '...' }\\`. Consult \\`loam instructions loam-spine\\` before editing\na join.\n\n## Reference pages — fetch when needed\n\n- \\`loam instructions loam-codes\\` — finding/refusal inventory and fixes.\n- \\`loam instructions loam-spine\\` — operation, message, coverage, permission,\n capability and baseline joins.\n- \\`loam instructions loam-authoring\\` — arch requirements, generated Gherkin\n and frontmatter/vouch grammar.\n- \\`loam instructions loam-done-check\\` — verification evidence and verdicts.\n\nBare \\`loam instructions\\` lists the six workflows, the separate \\`loam-report\\`\nsupport protocol and these references. Generated agent command/skill files are\nsmall pointers to them, so the runtime page wins if an old pointer disagrees with\nthe installed binary. MCP clients can read the same pages as\n\\`loam://orientation\\` and \\`loam://instructions/<name>\\` resources.\n\n## Safety and hand-back\n\nloam's writers are journaled, but the lifecycle still matters. Do not run\n\\`loam vouch\\` for a human, do not invent evidence, do not archive before code is\nmerged, and do not edit generated files under \\`<gherkinDir>/loam/\\` by hand.\n\\`loam unarchive <FEAT>\\` is the supported undo. To abandon a feature that was\nnever archived, review it and use \\`git rm -r features/<FEAT>\\`; loam deliberately\nhas no destructive drop command.\n\nFinish by reporting remaining warnings, unverifiable claims, files or directories\nyou did not inspect, and the exact human action still owed.\n`;\n\nexport const AGENTS_MD = `${agentsStampLine(LOAM_VERSION)}\\n${ORIENTATION}`;\n"]}
|
|
1
|
+
{"version":3,"file":"agents-md.js","sourceRoot":"","sources":["../../../src/core/agent/agents-md.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEtD,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKnB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,eAAe,CAAC,YAAY,CAAC,KAAK,WAAW,EAAE,CAAC","sourcesContent":["/**\n * The small, always-loaded orientation contract for a loam docs repository.\n *\n * Detailed grammars and workflows live behind `loam instructions`: those pages\n * describe the binary that is about to run, while this file is written once and\n * may be loaded into every agent turn for years. Keep only enough here to form\n * the next question, find the relevant page, and avoid an unsafe write.\n */\nimport { agentsStampLine } from \"./agents-stamp.js\";\nimport { LOAM_VERSION } from \"../envelope/version.js\";\n\nconst ORIENTATION = `# Working in this docs repo\n\nThis is a **loam** docs repo: plain files are the shared source of truth for a governed software\nfleet's architecture, requirements, contracts and verification evidence. A service may be a modular monolith, a network service, a CLI or a worker.\nloam has no server or database. Delete it and the documents still make sense.\n\n## What is where\n\n\\`architecture/landscape.likec4\\` is the fleet map, and\n\\`architecture/adrs/\\` optional system-level decisions — only their links are graded.\n\\`architecture/subsystems.likec4\\` is GENERATED by \\`loam subsystem sync\\` from the\ndirectory tree under \\`services/\\` — one view per subsystem, a scoping aid, never\nhand-edited and never a second fleet map. A diagram you author belongs in the\nlandscape or in a file of your own name beside it.\n\\`services/<id>/\\` holds a boundary's living \\`model.likec4\\`, \\`spec.md\\`,\n\\`arch.spec.md\\`, OpenAPI, AsyncAPI, runbook and health facts.\n\\`features/<FEAT>/\\` holds a change in flight: \\`intent.md\\`, \\`delta.likec4\\`,\nper-service requirement and contract deltas, and \\`verification.yaml\\`.\n\\`features/archive/\\` is shipped history.\n\nLiving documents are the complete current state. A feature document is a diff\nagainst them, reviewed as a diff; \\`loam archive\\` folds it into living state.\nDo not hand-edit the living landscape to add a feature change.\n\n## Linking between documents\n\nDocuments join with **standard markdown links** whose targets are real relative\npaths, for example \\`[decision](../adrs/ADR-001.md)\\`. They render in pull-request\nreview instead of showing literal brackets,\nand make “does this link resolve” a filesystem question; \\`[[Wikilinks]]\\` would\nrequire shortest-unique-path guessing.\nObsidian supports markdown links, autocomplete and rename-tracking when its\nwikilink setting is off. \\`link.unresolved\\` grades missing targets and CASE\n**is** graded; links outside this repository, \\`#section\\` targets, a fenced code\nblock or an inline code span are deliberately not graded.\n\n## \\`loam.json\\` — the wiring\n\nEvery repository commits its own \\`loam.json\\`:\n\n\\`{ \"docsDir\": \"../docs\", \"service\": \"payment-service\", \"gherkinDir\": \"features\" }\\`\n\n\\`docsDir\\` is stored exactly as it was passed and resolved against the directory\nholding the \\`loam.json\\`, so a relative path survives a clone; an absolute one\nraises \\`doctor.docs-absolute\\`. \\`service\\` binds a service checkout to the one boundary\nit may write evidence for. \\`gherkinDir\\` is optional. \\`agentTools\\` and\n\\`agentProfile\\` record which agent surfaces and workflow subset init manages;\n\\`agentFiles\\` records digests so unchanged generated pointers can refresh while\ncustomized ones remain untouched. With no config, loam refuses with \\`no-config\\`\nrather than guessing.\n\n\\`loam init --docs <dir>\\` **joins** an existing docs repo. Add \\`--create\\` only\nwhen creating a new one; a typo must not scaffold a second source of truth.\n\n## Starting from chat\n\nThere are two equivalent entry points into the same version-matched protocol.\nPrefer the explicit one when a user wants a predictable lifecycle: invoke\n\\`/loam-feature <FEAT> \"<title>\"\\` to propose, \\`/loam-implement <FEAT> [service]\\`\nto build, \\`/loam-check\\` and \\`/loam-verify <FEAT>\\` to grade it, and\n\\`/loam-ship <FEAT>\\` to finish. Most hosts use that flat slash\nspelling; Gemini uses \\`/loam:feature\\`, Amazon Q uses \\`@loam-feature\\`, and a\nskills-only host can invoke \\`$loam-feature\\`. Natural-language requests may load\nthe matching Agent Skill instead. Once either entry point is selected, the agent\ndrives the internal status/edit/validate loop and asks the user only for a real\ndecision or human-only act; the user does not have to type every loam command.\n\nIf loam or an agent integration behaves unexpectedly, invoke \\`/loam-report\\` (or\nload \\`$loam-report\\` on a skills-only host). It collects a sanitized, reproducible\nMarkdown report under \\`loam-reports/\\` without repairing the repository, retrying a\nwriter or uploading anything. This is support, not a seventh lifecycle step.\n\n## The cycle\n\n0. **Wire the repo** — in each service checkout run\n \\`loam init --docs <path-to-docs-repo> --service <id>\\`, then \\`loam doctor\\`.\n1. **Adopt** — for an undocumented boundary load \\`loam instructions loam-adopt\n <id>\\`, then use \\`loam adopt --service <id> --json\\`. Write drafts; a person,\n never an agent, later vouches for them.\n2. **Understand** — start or resume with \\`loam status --json\\`; its ordered\n \\`next[]\\` is the work queue. Read \\`next[0].execution\\`: run it only when\n \\`runnable\\` is true; otherwise use \\`kind\\`, \\`cwd\\`, \\`needs\\` and \\`after\\`\n to complete the edit or external-repo step. Use \\`loam list --json\\`, \\`loam show <id> --json\\`\n and \\`loam explore <id> --json\\` before proposing a change. Never propose a\n change to a service you have not read.\n3. **Scaffold** — load \\`loam instructions loam-feature <FEAT>\\`, then run\n \\`loam new <FEAT> --title \"...\" --touches <service>\\`.\n4. **Author** — fill the scaffolded TODOs and run \\`loam rebase <FEAT>\\` after\n reading the living baselines. The exact joins and authoring grammars are in\n the reference pages below.\n5. **Check** — load \\`loam instructions loam-check --no-fix-tables\\`, run\n \\`loam validate <FEAT> --json\\`, and use \\`loam explain <code>\\` for reported\n codes. \\`loam dependencies --json\\` names features that must archive first.\n6. **Build** — load \\`loam instructions loam-implement <FEAT>\\`.\n \\`loam delta <FEAT> --service <id> --json\\` is one service's task; in that\n service checkout, \\`loam gherkin <FEAT>\\` emits its executable scenarios.\n7. **Verify** — \\`loam verify <FEAT> --json\\` is the checklist. Record evidence\n from each affected service's own checkout. Read\n \\`loam instructions loam-done-check\\` before recording anything.\n8. **Ship** — load \\`loam instructions loam-ship <FEAT>\\`; begin with\n \\`loam archive <FEAT> --dry-run --json\\`, then archive only after the code is\n merged and the plan is the one reviewed.\n\n## Reading loam's output\n\nEvery command takes \\`--json\\`. The envelope has \\`contractVersion\\`, \\`ok\\` and,\non refusal, \\`error.code\\`. Unknown flags are \\`invalid-option\\`; \\`--help\\` and\n\\`--version\\` pass through. Branch on stable codes, never message prose.\n\nValidation \\`findings[]\\` carry \\`severity\\`; archive-related findings also carry\n\\`gates\\`. Severity and gating are two different questions: errors invalidate,\nwhile \\`gates: true\\` makes archive refuse — archive is gated on coherence, so a\ngating finding stops it whatever its severity. Advisory warnings never block.\n\\`--approve\\` overrides the gating issues — only those that are judgement calls —\nand never a mechanical loss. For example \\`delta.requirement-not-merged\\` warns but gates;\n\\`living.requirement-outside-requirements\\` cannot be approved, while\n\\`openapi.op-modified\\` is reviewable.\n\n\\`loam status --json\\` orders executable next actions. Findings include structured\n\\`locations[]\\`; use their paths instead of extracting filenames from prose.\n\\`loam show <FEAT> --json\\` includes a \\`review\\` pack with intent, architecture,\ncontracts, dependencies, verification, blockers and the same next actions.\n\\`loam explain <code> --json\\`\nreturns one code's meaning and fix; \\`loam explain --codes --json\\` lists them.\nWhich codes an INVOCATION can raise is in \\`loam instructions loam-codes\\`.\n\n## The identity rule\n\nA service is the element whose \\`metadata { service '<id>' }\\` matches its\n\\`services/<id>/\\` directory. An exact element id is the legacy fallback. Never\ninfer identity from display names or containment. Relationships join to contracts\nthrough exact ids: OpenAPI \\`operationId\\`, requirement \\`Operations:\\`, and C4\n\\`metadata { op '...' }\\`. Consult \\`loam instructions loam-spine\\` before editing\na join.\n\n## Reference pages — fetch when needed\n\n- \\`loam instructions loam-codes\\` — finding/refusal inventory and fixes.\n- \\`loam instructions loam-spine\\` — operation, message, coverage, permission,\n capability and baseline joins.\n- \\`loam instructions loam-authoring\\` — arch requirements, generated Gherkin\n and frontmatter/vouch grammar.\n- \\`loam instructions loam-done-check\\` — verification evidence and verdicts.\n\nBare \\`loam instructions\\` lists the six workflows, the separate \\`loam-report\\`\nsupport protocol and these references. Generated agent command/skill files are\nsmall pointers to them, so the runtime page wins if an old pointer disagrees with\nthe installed binary. MCP clients can read the same pages as\n\\`loam://orientation\\` and \\`loam://instructions/<name>\\` resources.\n\n## Safety and hand-back\n\nloam's writers are journaled, but the lifecycle still matters. Do not run\n\\`loam vouch\\` for a human, do not invent evidence, do not archive before code is\nmerged, and do not edit generated files under \\`<gherkinDir>/loam/\\` by hand.\n\\`loam unarchive <FEAT>\\` is the supported undo. To abandon a feature that was\nnever archived, review it and use \\`git rm -r features/<FEAT>\\`; loam deliberately\nhas no destructive drop command.\n\nFinish by reporting remaining warnings, unverifiable claims, files or directories\nyou did not inspect, and the exact human action still owed.\n`;\n\nexport const AGENTS_MD = `${agentsStampLine(LOAM_VERSION)}\\n${ORIENTATION}`;\n"]}
|
package/dist/core/repo/paths.js
CHANGED
|
@@ -186,7 +186,7 @@ export function fleetAdrsDir(docsDir) {
|
|
|
186
186
|
* The GENERATED subsystem views — beside the fleet map because the LikeC4
|
|
187
187
|
* renderer merges the whole `architecture/` project, and a view can only
|
|
188
188
|
* `include` elements the landscape beside it defines. `loam subsystem sync`
|
|
189
|
-
* owns the bytes (`core/repo/tree/views.ts` renders them); nothing in loam
|
|
189
|
+
* owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam
|
|
190
190
|
* ever parses the file, and staleness against the tree is a byte compare.
|
|
191
191
|
*/
|
|
192
192
|
export function subsystemViewsPath(docsDir) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: FeatureDir): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/core/repo/paths.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where every artifact lives. Artifact filenames are spelled here and nowhere\n * else, so a rename is one edit rather than a grep.\n *\n * This is also the module that carries loam's one path guarantee.\n * `unfiledServicePaths(docsDir, service)` spells `<docsDir>/services/<service>/`,\n * so both parts are caller-controlled path input, and `node:path` cannot help:\n * `join(...paths: string[])` accepts every string there is. The guarantee IS\n * the parameter brands and nothing else — `PathableService` for the id (a name\n * whose provenance is the repository, unconstructible from document text),\n * `DocsDir`/`FeatureDir` for the roots (a resolved config or validated\n * `--docs`; a directory an enumeration read), and `ServiceDir` for\n * `servicePathsAt` (the enumeration's own resolved directory, narrower still)\n * — the `kernel/ids/` package holds the only casts, so an unchecked value at\n * these call sites does not compile.\n * Code that spells `services/<id>/` or a feature layout with a bare join is\n * outside the guarantee — `commands/new.ts` and the openspec migrator both do,\n * held instead by `resolveInside` at the write.\n */\nimport { join } from \"node:path\";\nimport { serviceDirOf, type DocsDir, type FeatureDir, type ServiceDir } from \"../kernel/ids/dirs.js\";\nimport type { PathableService } from \"../kernel/ids/service.js\";\nimport type { SubsystemName } from \"../kernel/ids/subsystem.js\";\n\n/** Directory under features/ holding shipped features. Never a feature itself. */\nexport const ARCHIVE_DIR = \"archive\";\n\n/* ------------------------------------------------------------------ */\n/* Paths — artifact filenames are spelled here and nowhere else */\n/* ------------------------------------------------------------------ */\n\n/**\n * The living artifact filenames, as one table, because three questions read\n * them: `servicePathsAt` below spells the paths, the tree walk classifies\n * a directory as a service by whether any of these names sit in it, and\n * `core/diff/base-state.ts` classifies the SAME way over a base git ref's\n * tree listing (exported for that reader — a filename respelled there would\n * be the drift this table exists to prevent). The walk cannot import this\n * module — `repo → repo/tree` is the package direction, and an edge back\n * would be the cycle `scripts/package-graph.mjs` refuses — so `repo.ts`\n * injects `isServiceArtifactName` into the walk request instead.\n */\nexport const ARTIFACT_FILES = {\n model: \"model.likec4\",\n spec: \"spec.md\",\n archSpec: \"arch.spec.md\",\n openapi: \"openapi.yaml\",\n asyncapi: \"asyncapi.yaml\",\n runbook: \"runbook.md\",\n health: \"health.yaml\",\n steps: \"steps.yaml\",\n} as const;\nconst ADRS_DIR = \"adrs\";\n\n/** Does a directory entry name a living service artifact? The classification half of the table above. */\nexport function isServiceArtifactName(name: string, kind: \"file\" | \"dir\"): boolean {\n if (kind === \"dir\") return name === ADRS_DIR;\n return Object.values(ARTIFACT_FILES).some((file) => file === name);\n}\n\nexport interface ServicePaths {\n dir: ServiceDir;\n model: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract — the event axis's sibling of `openapi`. */\n asyncapi: string;\n runbook: string;\n health: string;\n /**\n * The step catalogue — which phrases this service's suite has DECIDED to\n * define. Authored and opt-in; its CONTENTS are read by `loam steps` alone and\n * nothing in `validate` consults them\n * (`core/gherkin/steps/catalogue.ts` records why).\n *\n * Its EXISTENCE is read more widely, and that is not the same statement: the\n * filename is in `ARTIFACT_FILES`, so `isServiceArtifactName` classifies it\n * and the tree walk treats a directory holding one as a service. That is\n * correct — it is a service artifact — and it is worth writing down, because\n * it means a `steps.yaml` written at the wrong path mints a service.\n */\n steps: string;\n adrsDir: string;\n}\n\n/**\n * The artifact paths of a service whose directory is already KNOWN — the\n * enumeration's `dir`, at whatever depth the tree walk found it. This is the\n * spelling every reader of an existing service must use: joining\n * `services/<id>/` at the root is only true for a fleet nobody has filed, and\n * a moved service would silently grade as absent through it.\n */\nexport function servicePathsAt(dir: ServiceDir): ServicePaths {\n return {\n dir,\n model: join(dir, ARTIFACT_FILES.model),\n spec: join(dir, ARTIFACT_FILES.spec),\n archSpec: join(dir, ARTIFACT_FILES.archSpec),\n openapi: join(dir, ARTIFACT_FILES.openapi),\n asyncapi: join(dir, ARTIFACT_FILES.asyncapi),\n runbook: join(dir, ARTIFACT_FILES.runbook),\n health: join(dir, ARTIFACT_FILES.health),\n steps: join(dir, ARTIFACT_FILES.steps),\n adrsDir: join(dir, ADRS_DIR),\n };\n}\n\n/**\n * The UNFILED spelling — `<docsDir>/services/<id>/`, the tree's root level.\n * This is where creation lands (`adopt`'s brief, a new service materialised by\n * `archive`, the OpenSpec migration) and the honest fallback for a service the\n * enumeration does not answer; for a service that EXISTS, resolve through the\n * enumeration instead (`servicePathsAt(entry.dir)`, or\n * `locateServicePaths` in `service-target.ts`, which chooses between the two).\n * Renamed from `servicePaths` when the tree landed, precisely so the compiler\n * would put every remaining caller in front of a reviewer: a root join that\n * survives unreviewed is a moved service silently grading as absent.\n */\nexport function unfiledServicePaths(docsDir: DocsDir, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(docsDir, \"services\", service)));\n}\n\n/**\n * The creation spelling INSIDE a subsystem — `<subsystemDir>/<id>/`, the\n * target `adopt --subsystem` briefs so an adoption need not land unfiled and\n * cost a second command. The same two provenances the unfiled spelling\n * combines, one level down: the subsystem directory came off the tree walk's\n * readdir, and the id passed the service grammar before it reached here.\n */\nexport function servicePathsUnder(subsystemDir: string, service: PathableService): ServicePaths {\n return servicePathsAt(serviceDirOf(join(subsystemDir, service)));\n}\n\n/**\n * The directory a SUBSYSTEM occupies (or will occupy) under a parent — the\n * spelling `subsystem new` creates and `subsystem rename` renames into. The\n * same guarantee-by-brand as the service spellings above: the parent came off\n * the tree walk's readdir (or is `servicesDir` itself), and demanding\n * `SubsystemName` here is what makes the validator unskippable — a raw argv\n * string at this call site does not compile, so no future verb can hoist the\n * join above the check that earns it.\n */\nexport function subsystemPathUnder(parentDir: string, name: SubsystemName): string {\n return join(parentDir, name);\n}\n\n/**\n * The pair of requirement-carrying spec files, living and delta alike: the\n * business spec and the architecture spec. Same grammar, same delta algebra,\n * same merge — everything that walks \"the spec files\" of a service walks this\n * list, so the two axes cannot drift apart in what handles them. `key` indexes\n * ServicePaths/FeatureSpecPaths; `label` is how the axis names itself in prose.\n */\nexport const SPEC_AXES = [\n { key: \"spec\", file: \"spec.md\", label: \"requirements\" },\n { key: \"archSpec\", file: \"arch.spec.md\", label: \"arch requirements\" },\n] as const;\nexport type SpecAxis = (typeof SPEC_AXES)[number];\n\nexport interface FeaturePaths {\n dir: string;\n intent: string;\n delta: string;\n specsDir: string;\n adrsDir: string;\n}\n\nexport function featurePaths(featureDir: FeatureDir): FeaturePaths {\n return {\n dir: featureDir,\n intent: join(featureDir, \"intent.md\"),\n delta: join(featureDir, \"delta.likec4\"),\n specsDir: join(featureDir, \"specs\"),\n adrsDir: join(featureDir, \"adrs\"),\n };\n}\n\nexport interface FeatureSpecPaths {\n dir: string;\n spec: string;\n archSpec: string;\n openapi: string;\n /** The async contract delta — the event axis's sibling of `openapi`. */\n asyncapi: string;\n}\n\nexport function featureSpecPaths(featureDir: FeatureDir, service: PathableService): FeatureSpecPaths {\n const dir = join(featureDir, \"specs\", service);\n return {\n dir,\n spec: join(dir, \"spec.md\"),\n archSpec: join(dir, \"arch.spec.md\"),\n openapi: join(dir, \"openapi.yaml\"),\n asyncapi: join(dir, \"asyncapi.yaml\"),\n };\n}\n\nexport function landscapePath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", LANDSCAPE_FILE);\n}\n\n/** The fleet's authorization vocabulary — beside the fleet map, for the same reason. */\nexport function permissionsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"permissions.yaml\");\n}\n\n/** The fleet's capability vocabulary — beside the other two fleet documents, for the same reason. */\nexport function capabilitiesPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"capabilities.yaml\");\n}\n\n/**\n * The fleet's ARCHITECTURAL OBLIGATION vocabulary — beside the landscape it tags\n * and the two vocabularies it sits with, for their reason: it is a fact about\n * the fleet that no single service owns.\n */\nexport function obligationsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", \"obligations.yaml\");\n}\n\n/**\n * The FLEET's decision records — `architecture/adrs/`, beside the landscape and\n * the two vocabularies.\n *\n * ADRs used to exist at two altitudes only, `services/<id>/adrs/` and\n * `features/<FEAT>/adrs/`, so a decision about the fleet itself — \"event\n * publishers use a transactional outbox\", \"cross-service calls carry a circuit\n * breaker\" — had no home but one arbitrary service's directory, where the next\n * reader of the other fifty services never finds it. This is not a new axis: it\n * is the same `ADRS_DIR` name one level up, spelled through the same constant so\n * the two altitudes cannot drift into `adrs/` and `decisions/`.\n *\n * PRESENCE-TRACKED AND NOTHING MORE, exactly like the service directory it\n * mirrors: `loam list` counts the files (`repo.ts`'s `fleetAdrCount`) and no\n * check anywhere reads one. A fleet with no fleet-level ADRs owes nothing and\n * must produce no finding — which is also why `loam init` does NOT scaffold the\n * directory. git does not carry an empty one, so it would vanish on the first\n * clone and come back as a diff on the next `init`; and an empty `adrs/` in a\n * fresh repo reads as an obligation nobody has met, when there is no obligation\n * at all.\n */\nexport function fleetAdrsDir(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", ADRS_DIR);\n}\n\n/**\n * The GENERATED subsystem views — beside the fleet map because the LikeC4\n * renderer merges the whole `architecture/` project, and a view can only\n * `include` elements the landscape beside it defines. `loam subsystem sync`\n * owns the bytes (`core/repo/tree/render/views.ts` renders them); nothing in loam\n * ever parses the file, and staleness against the tree is a byte compare.\n */\nexport function subsystemViewsPath(docsDir: DocsDir): string {\n return join(docsDir, \"architecture\", SUBSYSTEM_VIEWS_FILE);\n}\n\n/** The generated views' file name, spelled once because the reservation below reads it too. */\nconst SUBSYSTEM_VIEWS_FILE = \"subsystems.likec4\";\n\n/**\n * Names inside `architecture/` that LOAM owns, and what goes wrong if a\n * feature's own document claims one.\n *\n * `features/<FEAT>/deployment/<name>.likec4` merges as a whole-file copy\n * straight into `architecture/` — unlike a flow, which lands under\n * `usecases/` — so its `<name>` is free to be any of these, and two of them\n * fail in ways nothing downstream would report:\n *\n * - `subsystems.likec4` is GENERATED and is EXCLUDED from every project load\n * (docs/DESIGN.md rule 26 keeps it a byte compare), so topology landing\n * there is invisible to loam from the moment it archives — and the next\n * `loam subsystem sync` overwrites the file wholesale. Silent both ways,\n * which is the worst shape a failure can have;\n * - `landscape.likec4` is the living map the archive SPLICES; a whole-file\n * copy over it would discard the fleet;\n * - `usecases/` is the flow tree, walked by the use-case scan. A deployment\n * document filed there parses (one project) and is then looked for by a\n * reader asking a different question.\n *\n * Returns the reason, or null when the name is the author's to choose. Takes\n * the `/`-separated rel a directory walk produced, never a string from argv.\n */\nexport function reservedArchitectureName(rel: string): string | null {\n const parts = rel.split(\"/\");\n if (parts[0] === USECASE_DIR) {\n return `\\`${USECASE_DIR}/\\` is the use-case tree — a flow belongs in \\`features/<FEAT>/usecases/\\`, which merges there`;\n }\n if (rel === SUBSYSTEM_VIEWS_FILE) {\n return \"`subsystems.likec4` is generated by `loam subsystem sync`, which overwrites it wholesale — and nothing in loam ever parses it, so anything landing there is invisible from the moment it archives\";\n }\n if (rel === LANDSCAPE_FILE) {\n return \"`landscape.likec4` is the living fleet map, which the archive SPLICES into rather than replaces\";\n }\n return null;\n}\n\n/** The living map's file name, and the flow tree's directory — spelled once, read by the reservation. */\nconst LANDSCAPE_FILE = \"landscape.likec4\";\nconst USECASE_DIR = \"usecases\";\n\nexport function featuresDir(docsDir: DocsDir): string {\n return join(docsDir, \"features\");\n}\n\n/**\n * The root of the tree that IS the fleet — where an unfiled service and a\n * root-level subsystem both sit. The `subsystem` verbs join validated names\n * under it; the enumerating walk spells the same join for itself\n * (`repo/tree/walk.ts`), because it cannot import this module.\n */\nexport function servicesDir(docsDir: DocsDir): string {\n return join(docsDir, \"services\");\n}\n\n/**\n * The agent contract's filename, and where it sits in a docs repo.\n *\n * Both spellings are exported because the two questions are different: `init`,\n * `doctor` and `validate` want a path under a docsDir they already hold, while\n * `docs.ts` builds the scaffold as `[relative path, bytes]` pairs and needs the\n * bare name. Four modules spelled the literal out for themselves, and one of\n * them — `init` — uses it to decide whether a directory IS a docs repo at all,\n * so a rename that reached three of the four would leave `init` recognising a\n * file nothing else writes any more.\n *\n * `openspec-inventory.ts`'s \"AGENTS.md\" is deliberately NOT this constant: it\n * names a file in a foreign OpenSpec tree that happens to share the name.\n */\nexport const AGENTS_FILENAME = \"AGENTS.md\";\n\nexport function agentsPath(docsDir: DocsDir): string {\n return join(docsDir, AGENTS_FILENAME);\n}\n\n/**\n * Where shipped features live. `features/archive/` is spelled here and nowhere\n * else for the same reason every other artifact path is: `archive` and\n * `unarchive` move directories in and out of it, and a layout string that two\n * commands re-derive is a layout two commands can disagree about. The\n * `features/archive/…` strings in refusal PROSE are not this path — they are\n * what a reader types into `ls`, and they stay spelled out.\n */\nexport function archiveDir(docsDir: DocsDir): string {\n return join(featuresDir(docsDir), ARCHIVE_DIR);\n}\n"]}
|