@mmerterden/multi-agent-pipeline 16.5.0 → 16.7.0

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +4 -4
  3. package/README.tr.md +4 -4
  4. package/docs/architecture.md +2 -2
  5. package/docs/ecosystem.md +5 -5
  6. package/package.json +1 -1
  7. package/pipeline/commands/multi-agent/analysis/SKILL.md +18 -5
  8. package/pipeline/commands/multi-agent/feedback/SKILL.md +51 -0
  9. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +32 -0
  10. package/pipeline/commands/multi-agent/sync/SKILL.md +20 -18
  11. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  12. package/pipeline/multi-agent-refs/analysis/intake.md +30 -1
  13. package/pipeline/multi-agent-refs/analysis/locked.md +11 -6
  14. package/pipeline/multi-agent-refs/analysis/render.md +20 -5
  15. package/pipeline/multi-agent-refs/analysis/review.md +86 -0
  16. package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
  17. package/pipeline/multi-agent-refs/analysis-template-corporate.md +436 -0
  18. package/pipeline/multi-agent-refs/analysis-template.md +31 -13
  19. package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -7
  20. package/pipeline/multi-agent-refs/website-deploy.md +87 -0
  21. package/pipeline/schemas/analysis-spec.schema.json +21 -1
  22. package/pipeline/schemas/prefs.schema.json +41 -0
  23. package/pipeline/scripts/build-references.mjs +368 -0
  24. package/pipeline/scripts/feedback-send.mjs +181 -0
  25. package/pipeline/scripts/validate-analysis-doc.mjs +130 -9
  26. package/pipeline/scripts/website-deploy-commit.sh +102 -0
  27. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +18 -2
  28. package/pipeline/skills/shared/core/multi-agent-feedback/SKILL.md +30 -0
  29. package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +31 -0
  30. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +18 -14
@@ -42,7 +42,8 @@ Every per-platform file starts with this YAML block:
42
42
  ```yaml
43
43
  ---
44
44
  feature: <FeatureName>
45
- platform: ios | android | backend | frontend
45
+ platform: ios | android | backend | frontend | none
46
+ profile: global | corporate
46
47
  language: tr | en
47
48
  mode: full | lite
48
49
  ui_tests: true | false
@@ -63,6 +64,8 @@ template_version: v3
63
64
 
64
65
  `ui_tests` and `a11y_depth` record the Phase 0 Step 5a opt-ins (defaults `false` / `basic`) so the coverage choice is auditable and the pre-dispatch validator can enforce it: `ui_tests: true` requires Section 15.6, `a11y_depth: full` requires the Section 16.2 walkthrough.
65
66
 
67
+ `profile` names the template the document was rendered against (Locked 32) and `platform: none` marks the stack-optional render (Locked 35). Both are read by `validate-analysis-doc.mjs`, which applies a different contract per profile: without the key a corporate document would be judged against the global rules and its backbone would read as a pile of Locked 2 violations.
68
+
66
69
  Phase 1 compares `evidence_digest` against an existing document to decide whether to reuse it (Locked 27). Phase 3 reads `platform` to verify file match, `mode` to know which section set to expect, and both `evidence_digest` and `base_commit` to judge freshness: the digest says the evidence changed, `base_commit` says the repo moved. Phase 2 parses the block but gates only on `template_version`.
67
70
 
68
71
  ## Layer headings (A / B / C)
@@ -912,25 +915,40 @@ Auto-populated rows (Locked 11, 23): repo-evidence direct-match candidates that
912
915
 
913
916
  ## 21. Referanslar / References
914
917
 
915
- Never omitted. Locked 20 - at the bottom, not at the top.
918
+ Never omitted (Locked 21 - at the bottom, not at the top). Shared verbatim by both profiles.
919
+
920
+ **Built deterministically, not written by the model.** `~/.claude/scripts/build-references.mjs` reads `state.analysisSpec.evidence.*` and emits this table. A hand-written references table drifts from what the run actually read: it lists what the author remembers consulting, which is a different set from what the evidence gathering fetched. Every row here is a source the run touched.
916
921
 
917
922
  ```markdown
918
923
  ## 21. Referanslar <!-- TR -->
919
924
  ## 21. References <!-- EN -->
920
925
 
921
- | Tür / Type | Kaynak / Source | URL or Path | Rol / Role | Notlar / Notes |
922
- |---|---|---|---|---|
923
- | Standards | <name> | <url or path> | binding | <version> |
924
- | Wiki | <name> | <url> | binding | <pattern referenced> |
925
- | Figma | <design name> | <url> | UI design | <n variants, all screenshots embedded> |
926
- | Confluence | <spec name> | <url> | feature spec | <v<n>> |
927
- | Confluence | <api contract> | <url> | API contract | <endpoint summary> |
928
- | Jira | <ticket id> | <url> | ticket | <description> |
929
- | Firebase | events | <console url> | reference only | auth-gated |
930
- | OpenAPI | generated | <repo path> | DTO source | <regen plugin name> |
931
- | Repo | <module> | <repo path> | existing implementation | <reuse summary> |
926
+ | Tür / Type | Kaynak / Source | URL / Yol | Sürüm / Ref | Rol / Role | Erişim / Access | Notlar / Notes |
927
+ |---|---|---|---|---|---|---|
928
+ | Figma | <design name> | <url> | node-id=<nodeId> | UI design | ok (Tier <n>) | <n frames> |
929
+ | Confluence | <spec name> | <url> | pageId=<id> v<n> | feature spec | ok | - |
930
+ | Confluence | <api contract> | <url> | pageId=<id> v<n> | API contract | ok | <endpoint summary> |
931
+ | Jira | <ticket id> | <url> | - | ticket | ok | <summary> |
932
+ | Swagger | <api name> | <url> | <spec version> | API contract | ok | <n endpoints> |
933
+ | Repo | <module> | <repo path> | <commit sha> | existing implementation | ok | <reuse summary> |
934
+ | Standards | <name> | <url or path> | <version> | binding | ok | <kind> |
935
+ | Firebase | events | <console url> | - | reference only | erişilemedi (auth) | <n events> |
936
+ | Doküman | <file name> | <local path> | <format> | scope document | ok | - |
937
+ | Dış kaynak | <name> | <url> | <citation> | referans | ok | <claim> |
938
+ | Serbest metin | kullanıcı notu | - | - | <what it settled> | - | "<verbatim quote>" |
939
+ | Confluence | <unreachable page> | <url> | - | getirilemedi | erişilemedi (403) | - |
932
940
  ```
933
941
 
942
+ These row types are exactly what `build-references.mjs` emits, and the example is kept in step with it deliberately: a shape shown here but never produced would send a reader hand-checking against a table that cannot exist. A wiki source arrives as a `Standards` row carrying `wiki` in its `kind` cell, and a generated OpenAPI client arrives as the `Repo` row of the module that holds it; neither has a row type of its own.
943
+
944
+ **Column contract.**
945
+
946
+ - **Sürüm / Ref** is the precision anchor: the node id for a Figma frame, `pageId` plus page version for Confluence, the commit SHA the repo was read at, the spec version for Swagger. Without it a reference points at a moving target, and a reader six weeks later cannot tell whether the document described what they are looking at.
947
+ - **Erişim / Access** is `ok` or `erişilemedi (<reason>)`. A source that was declared but could not be fetched still gets a row. Dropping it hides the gap: the reader sees a document that never mentions the API contract and assumes there was none, rather than knowing it was unreachable.
948
+ - **Serbest metin** rows carry what the user stated in conversation that no fetched source contains, quoted verbatim, with the decision it settled in the `Rol` column. Scope decisions made in chat are evidence; leaving them out is how a document loses the reason it excluded something.
949
+
950
+ **Coverage gate (Locked 34).** Before the document is emitted, the validator compares this table against the evidence record. Every entry in `evidence.figma[]`, `evidence.confluence[]`, `evidence.jira[]`, `evidence.swagger[]`, `evidence.repo[]`, `evidence.standards[]`, `evidence.firebase[]`, `evidence.documents[]`, `evidence.outside[]`, `evidence.freeText[]` and every entry in `evidence.fetchErrors[]` must appear as a row. A source that shaped the document but is missing from References fails the dispatch gate, and a row with no matching evidence entry fails it too - an invented reference is worse than a missing one.
951
+
934
952
  ## 22. Sözlük / Glossary
935
953
 
936
954
  Footer. Optional in Lite mode. Alphabetical.
@@ -6,15 +6,18 @@
6
6
 
7
7
  ---
8
8
 
9
- ## 1. Command Inventory (51 files, 47 live commands)
9
+ ## 1. Command Inventory (53 files, 49 live commands)
10
10
 
11
11
  ```
12
- analysis, analysis-resolve, autopilot, build-optimize, channels, complaint-analysis, create-jira, design-check, dev,
13
- dev-autopilot, dev-local, dev-local-autopilot, diff-explain, forget, garbage-collect,
14
- help, ios-coding-standard, issue, jira, kill, language, local,
15
- local-autopilot, log, manual-test, prune-logs, prune-prompts, purge, refactor, resume, review, review-issue, review-jira,
16
- routines, save, scan, search, setup, resume-local, stack, status, store-ready, sync, test, test-accessibility,
17
- test-dark-mode, test-dynamic-type, test-screenshots, testflight-validation, uninstall, update
12
+ analysis, analysis-resolve, autopilot, build-optimize, channels,
13
+ complaint-analysis, create-jira, design-check, dev, dev-autopilot, dev-local,
14
+ dev-local-autopilot, diff-explain, feedback, forget, garbage-collect, help,
15
+ ios-coding-standard, issue, jira, kill, language, local, local-autopilot,
16
+ log, manual-test, prune-logs, prune-prompts, purge, refactor, resume,
17
+ resume-local, review, review-analysis, review-issue, review-jira, routines,
18
+ save, scan, search, setup, stack, status, store-ready, sync, test,
19
+ test-accessibility, test-dark-mode, test-dynamic-type, test-screenshots,
20
+ testflight-validation, uninstall, update
18
21
  ```
19
22
 
20
23
  Categories:
@@ -0,0 +1,87 @@
1
+ # Website deploy: why the commit author decides whether the site updates
2
+
3
+ Loaded by `/multi-agent:sync` Step 4. Read it when a website sync pushed cleanly
4
+ and the live site did not change.
5
+
6
+ ## The failure
7
+
8
+ The deploy platform builds a commit only when its author is a contributor on the
9
+ project. A commit carrying any other identity is accepted by `git push` and then
10
+ never built:
11
+
12
+ ```
13
+ vercel ls -> Status UNKNOWN Duration ? Builds: . [0ms]
14
+ API -> readyState: "BLOCKED"
15
+ readyStateReason: "The Deployment was blocked because the commit
16
+ author does not have contributing access ..."
17
+ seatBlock: { blockCode: "TEAM_ACCESS_REQUIRED", gitProvider: "github" }
18
+ ```
19
+
20
+ Nothing in the push output says so, the deployment exists, and the site keeps
21
+ serving the previous version. Pipeline v16.4.0 and v16.5.0 were both pushed this
22
+ way; neither was ever built, and both syncs reported the website as done.
23
+
24
+ ## The rule
25
+
26
+ The identity is the one `prefs.global.identities[]` routes to the website owner
27
+ through `platformIdentityRouting`, which is not always the account the current run
28
+ is working under. A run driven from a work account, or an exported
29
+ `GIT_AUTHOR_EMAIL`, is exactly how the wrong author gets recorded.
30
+
31
+ `$HOME/.claude/scripts/website-deploy-commit.sh` applies the rule:
32
+
33
+ 1. Read the clone's own `user.name` / `user.email` and write only on a mismatch.
34
+ The website clone is usually already configured correctly; overwriting it with
35
+ the caller's identity is the defect, not the fix.
36
+ 2. Commit only when something is staged.
37
+ 3. Read the author back off the commit with `git log -1 --format=%ae`. Setting
38
+ `git config` is not proof: an exported `GIT_AUTHOR_EMAIL` outranks it. On a
39
+ mismatch the script halts before pushing, so the bad commit stays local.
40
+ 4. Push, then wait for a Ready production build. A push is not a deploy.
41
+
42
+ Exit codes: `0` committed and pushed (or nothing to do), `1` wrong author and
43
+ nothing pushed, `2` usage or environment, `3` pushed but no Ready build.
44
+
45
+ Env: `WEBSITE_SYNC_NO_PUSH=1` (local only), `WEBSITE_SYNC_NO_VERIFY=1` (skip the
46
+ deployment check), `WEBSITE_SYNC_WAIT=<sec>` (default 45).
47
+
48
+ ## Recovery when a deployment is already blocked
49
+
50
+ History does not need rewriting, and `main` is never force-pushed. The platform
51
+ checks the HEAD commit of each new deployment, so a fresh commit under the right
52
+ identity is enough:
53
+
54
+ ```bash
55
+ git commit --allow-empty -m "chore(site): redeploy under the maintainer identity"
56
+ git push origin main
57
+ ```
58
+
59
+ Or deploy from the CLI, which attaches no rejected author:
60
+
61
+ ```bash
62
+ cd "$WEBSITE_DIR" && vercel --prod --yes
63
+ ```
64
+
65
+ Blocked deployments can be left in place; they hold no alias.
66
+
67
+ ## Diagnosing
68
+
69
+ The CLI prints `UNKNOWN` and hides the reason; the API gives it:
70
+
71
+ ```
72
+ GET https://api.vercel.com/v13/deployments/<dpl_id>?teamId=<team_id>
73
+ -> readyState, readyStateReason, seatBlock
74
+ ```
75
+
76
+ Read the CLI token out of its own auth file into a variable. Never into argv, a
77
+ log or a reply.
78
+
79
+ ## Verifying the live site
80
+
81
+ - The repo directory name is not the domain. Check the domain the project
82
+ actually serves, not `$HOME/{website-host}`.
83
+ - Version strings and counts are server-rendered and appear in the initial HTML.
84
+ Feature prose and lazily-loaded components do not: grep the JS chunks for those.
85
+ - A `curl` on the HTML can return 403 bot mitigation (`x-vercel-mitigated:
86
+ challenge`) rather than the page, which reads like content that never shipped.
87
+ Static assets are not challenged.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/{owner}/multi-agent-pipeline/pipeline/schemas/analysis-spec.schema.json",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "title": "Multi-Agent Pipeline - /multi-agent:analysis spec output (v3 template)",
6
6
  "description": "Contract for the feature-spec analysis document generated by /multi-agent:analysis. Platform-agnostic concept layer + Pass B per-platform render with repo-driven conventions. 23 sections in Full mode; 8 of them in Lite mode. Sections may be absent - omission is the policy when no evidence exists.",
7
7
  "type": "object",
@@ -49,6 +49,12 @@
49
49
  "default": "v3",
50
50
  "description": "Template version emitted. v3 is the platform-agnostic + Pass B render template (Locked 22)."
51
51
  },
52
+ "profile": {
53
+ "type": "string",
54
+ "enum": ["global", "corporate"],
55
+ "default": "global",
56
+ "description": "Analysis profile chosen at Phase 0 Step 1b (Locked 32). 'global' renders analysis-template.md (23-section development handoff); 'corporate' renders analysis-template-corporate.md (IG/UC/FG requirements document). Both read the same evidence; only the projection differs."
57
+ },
52
58
  "options": {
53
59
  "type": "object",
54
60
  "additionalProperties": false,
@@ -523,6 +529,20 @@
523
529
  }
524
530
  }
525
531
  },
532
+ "freeText": {
533
+ "type": "array",
534
+ "description": "User statements made in conversation that no fetched source carries, recorded so Section 21 can cite the decisions they settled (Locked 34).",
535
+ "items": {
536
+ "type": "object",
537
+ "additionalProperties": false,
538
+ "required": ["text"],
539
+ "properties": {
540
+ "label": { "type": "string" },
541
+ "text": { "type": "string" },
542
+ "role": { "type": "string" }
543
+ }
544
+ }
545
+ },
526
546
  "fetchErrors": {
527
547
  "type": "array",
528
548
  "description": "Phase 1 access failures (401/403/login-redirect). Surfaced as warnings, not blockers.",
@@ -821,6 +821,47 @@
821
821
  "default": false,
822
822
  "description": "v6.1.0+ - Phase 4 Step 2.5 rebuttal round. When reviewers disagree (mixed blocker/approved verdict), each reviewer is re-prompted with the others' opposing arguments for one additional round before triage. Lifts signal quality on ambiguous findings at ~1\u00d7 Step 2 token cost. Off by default - flip for security-critical or release-branch reviews."
823
823
  },
824
+ "analysisProfiles": {
825
+ "type": "array",
826
+ "description": "v16.6+ - which analysis standards the /multi-agent:analysis Step 1b picker offers (Locked 32). Listing one value auto-resolves the step instead of asking a question whose answer is already settled. Omitted means both are offered.",
827
+ "items": {
828
+ "type": "string",
829
+ "enum": ["global", "corporate"]
830
+ },
831
+ "default": ["global", "corporate"],
832
+ "uniqueItems": true,
833
+ "minItems": 1
834
+ },
835
+ "analysisProfile": {
836
+ "type": "object",
837
+ "additionalProperties": false,
838
+ "description": "v16.6+ - per-profile deployment bindings. These are site configuration, not part of the shipped template: an unconfigured run still renders the full document and asks for its destination at Phase 3.5 like any other run. Keys are deliberately generic so no organisation's space, page or tooling names live in the repo.",
839
+ "properties": {
840
+ "corporate": {
841
+ "type": "object",
842
+ "additionalProperties": false,
843
+ "description": "Bindings for the corporate profile: where its documents are published and which house terms they use.",
844
+ "properties": {
845
+ "confluenceSpaceKey": {
846
+ "type": "string",
847
+ "description": "Space the analysis page is created in when the Phase 3.5 picker chooses Confluence."
848
+ },
849
+ "confluenceParentPageId": {
850
+ "type": "string",
851
+ "description": "Parent page the analysis is filed under."
852
+ },
853
+ "titleFormat": {
854
+ "type": "string",
855
+ "description": "Page-title pattern, e.g. \"{prefix}{module}_{flow}_{suffix}\". Placeholders are resolved at emit time."
856
+ },
857
+ "titlePrefix": {
858
+ "type": "string",
859
+ "description": "Prefix that marks pipeline-authored pages so they stay distinguishable from hand-written ones."
860
+ }
861
+ }
862
+ }
863
+ }
864
+ },
824
865
  "updateCheck": {
825
866
  "type": "object",
826
867
  "additionalProperties": false,
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ // build-references.mjs - deterministic Section 21 References table for an
3
+ // /multi-agent:analysis document (Locked 34).
4
+ //
5
+ // The references table used to be prose the model filled in. That lists what an
6
+ // author remembers consulting, which is a different set from what the run
7
+ // actually fetched: sources that failed to load vanish silently, scope decisions
8
+ // made in conversation never appear, and a Figma link with no node id points at
9
+ // whatever the file looks like today. This turns the table into a projection of
10
+ // `state.analysisSpec.evidence.*` so the two cannot disagree.
11
+ //
12
+ // Zero deps. Two modes:
13
+ //
14
+ // build (default) state JSON -> markdown table on stdout
15
+ // --check <doc.md> state JSON + emitted doc -> coverage gate, exit 1 on drift
16
+ //
17
+ // Usage:
18
+ // node build-references.mjs state.json [--lang tr|en]
19
+ // node build-references.mjs state.json --check analysis/Feature-ios.md
20
+ // cat state.json | node build-references.mjs - --lang en
21
+ //
22
+ // Exit codes: 0 ok, 1 coverage failure, 2 usage / parse error.
23
+
24
+ import { readFileSync } from "node:fs";
25
+
26
+ const HEADERS = {
27
+ tr: ["Tür", "Kaynak", "URL / Yol", "Sürüm / Ref", "Rol", "Erişim", "Notlar"],
28
+ en: ["Type", "Source", "URL / Path", "Version / Ref", "Role", "Access", "Notes"],
29
+ };
30
+
31
+ const OK = { tr: "ok", en: "ok" };
32
+ const FAILED = { tr: "erişilemedi", en: "not reachable" };
33
+ const NONE = "-";
34
+
35
+ function argOf(argv, flag) {
36
+ const i = argv.indexOf(flag);
37
+ return i === -1 ? null : argv[i + 1] ?? null;
38
+ }
39
+
40
+ function readState(path) {
41
+ const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
42
+ const parsed = JSON.parse(raw);
43
+ // Accept either the full task state or the analysisSpec alone.
44
+ return parsed.analysisSpec ?? parsed;
45
+ }
46
+
47
+ // A cell that would break the pipe table, or read as an empty column, is worse
48
+ // than a visibly absent value: the reader cannot tell a missing cell from a
49
+ // cell that was never meant to have content.
50
+ function cell(value) {
51
+ if (value === null || value === undefined) return NONE;
52
+ const text = String(value).replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
53
+ return text === "" ? NONE : text;
54
+ }
55
+
56
+ function accessCell(ok, reason, lang) {
57
+ if (ok) return OK[lang];
58
+ return reason ? `${FAILED[lang]} (${cell(reason)})` : FAILED[lang];
59
+ }
60
+
61
+ // The identity of a source for coverage purposes. Two rows describing the same
62
+ // URL are the same source; a row with no locator falls back to its label so a
63
+ // free-text note still has something to match on.
64
+ function keyOf(url, fallback) {
65
+ const value = url && String(url).trim() !== "" && url !== NONE ? url : fallback;
66
+ return value ? String(value).trim() : null;
67
+ }
68
+
69
+ function rowsFrom(spec, lang) {
70
+ const ev = spec.evidence ?? {};
71
+ const rows = [];
72
+ const push = (type, source, url, ref, role, access, notes) => {
73
+ rows.push({
74
+ type,
75
+ cells: [cell(type), cell(source), cell(url), cell(ref), cell(role), access, cell(notes)],
76
+ key: keyOf(url, source),
77
+ });
78
+ };
79
+
80
+ for (const f of ev.figma ?? []) {
81
+ const ref = f.nodeId ? `node-id=${f.nodeId}` : f.fileKey ? `fileKey=${f.fileKey}` : NONE;
82
+ const frames = Array.isArray(f.frames) ? f.frames.length : 0;
83
+ const tier = f.tier ? ` (Tier ${f.tier})` : "";
84
+ push(
85
+ "Figma",
86
+ f.frames?.[0]?.name ?? f.fileKey ?? "design",
87
+ f.url,
88
+ ref,
89
+ lang === "tr" ? "UI tasarım" : "UI design",
90
+ OK[lang] + tier,
91
+ frames ? (lang === "tr" ? `${frames} frame` : `${frames} frames`) : NONE,
92
+ );
93
+ }
94
+
95
+ for (const c of ev.confluence ?? []) {
96
+ const ref = [c.pageId ? `pageId=${c.pageId}` : null, c.version ? `v${c.version}` : null]
97
+ .filter(Boolean)
98
+ .join(" ");
99
+ push(
100
+ "Confluence",
101
+ c.title ?? c.pageId,
102
+ c.url,
103
+ ref || NONE,
104
+ c.embeddedApiTable
105
+ ? lang === "tr" ? "API kontratı" : "API contract"
106
+ : lang === "tr" ? "özellik spesifikasyonu" : "feature spec",
107
+ OK[lang],
108
+ NONE,
109
+ );
110
+ }
111
+
112
+ for (const j of ev.jira ?? []) {
113
+ push("Jira", j.id, j.url ?? NONE, NONE, lang === "tr" ? "ticket" : "ticket", OK[lang], j.summary);
114
+ }
115
+
116
+ for (const s of ev.swagger ?? []) {
117
+ const n = Array.isArray(s.endpoints) ? s.endpoints.length : 0;
118
+ push(
119
+ "Swagger",
120
+ s.title ?? s.url,
121
+ s.url,
122
+ s.version ?? NONE,
123
+ lang === "tr" ? "API kontratı" : "API contract",
124
+ OK[lang],
125
+ n ? (lang === "tr" ? `${n} endpoint` : `${n} endpoints`) : NONE,
126
+ );
127
+ }
128
+
129
+ for (const r of ev.repo ?? []) {
130
+ push(
131
+ "Repo",
132
+ r.repoName,
133
+ r.path ?? r.repoName,
134
+ r.commit ?? r.sha ?? NONE,
135
+ lang === "tr" ? "mevcut kod" : "existing implementation",
136
+ OK[lang],
137
+ r.reuseSummary,
138
+ );
139
+ }
140
+
141
+ for (const s of ev.standards ?? []) {
142
+ push(
143
+ "Standards",
144
+ s.title ?? s.source,
145
+ s.source,
146
+ s.version ?? NONE,
147
+ s.binding ? "binding" : lang === "tr" ? "referans" : "reference",
148
+ OK[lang],
149
+ s.kind,
150
+ );
151
+ }
152
+
153
+ for (const f of ev.firebase ?? []) {
154
+ const n = Array.isArray(f.events) ? f.events.length : 0;
155
+ push(
156
+ "Firebase",
157
+ f.kind ?? "events",
158
+ f.source,
159
+ NONE,
160
+ lang === "tr" ? "referans" : "reference only",
161
+ OK[lang],
162
+ n ? (lang === "tr" ? `${n} event` : `${n} events`) : NONE,
163
+ );
164
+ }
165
+
166
+ for (const d of ev.documents ?? []) {
167
+ push(
168
+ lang === "tr" ? "Doküman" : "Document",
169
+ d.title ?? d.path,
170
+ d.path,
171
+ d.format ?? NONE,
172
+ d.binding ? "binding" : lang === "tr" ? "kapsam dokümanı" : "scope document",
173
+ accessCell(d.fetched !== false, d.fetched === false ? d.reason : null, lang),
174
+ NONE,
175
+ );
176
+ }
177
+
178
+ for (const o of ev.outside ?? []) {
179
+ push(
180
+ lang === "tr" ? "Dış kaynak" : "External",
181
+ o.source,
182
+ o.url ?? NONE,
183
+ o.citation ?? NONE,
184
+ lang === "tr" ? "referans" : "reference",
185
+ OK[lang],
186
+ o.claim,
187
+ );
188
+ }
189
+
190
+ for (const s of ev.signals ?? []) {
191
+ push(
192
+ lang === "tr" ? "Sinyal" : "Signal",
193
+ s.source,
194
+ s.url ?? NONE,
195
+ s.observedAt ?? NONE,
196
+ lang === "tr" ? "referans" : "reference",
197
+ OK[lang],
198
+ s.claim,
199
+ );
200
+ }
201
+
202
+ // Statements the user made in conversation that no fetched source carries.
203
+ // These settle scope as often as a document does, and a document that drops
204
+ // them loses the reason it excluded something.
205
+ for (const t of ev.freeText ?? []) {
206
+ push(
207
+ lang === "tr" ? "Serbest metin" : "Free text",
208
+ t.label ?? (lang === "tr" ? "kullanıcı notu" : "user note"),
209
+ NONE,
210
+ NONE,
211
+ t.role ?? (lang === "tr" ? "kapsam kararı" : "scope decision"),
212
+ NONE,
213
+ t.text ? `"${t.text}"` : NONE,
214
+ );
215
+ }
216
+
217
+ // A declared source that could not be fetched still gets a row. Dropping it
218
+ // reads to the next person as a source that never existed, which is the
219
+ // opposite of what happened.
220
+ for (const e of ev.fetchErrors ?? []) {
221
+ push(
222
+ e.type ?? (lang === "tr" ? "Kaynak" : "Source"),
223
+ e.title ?? e.url,
224
+ e.url,
225
+ NONE,
226
+ lang === "tr" ? "getirilemedi" : "not fetched",
227
+ accessCell(false, e.reason, lang),
228
+ NONE,
229
+ );
230
+ }
231
+
232
+ return rows;
233
+ }
234
+
235
+ function renderTable(rows, lang) {
236
+ const head = HEADERS[lang];
237
+ const lines = [`| ${head.join(" | ")} |`, `|${head.map(() => "---").join("|")}|`];
238
+ for (const r of rows) lines.push(`| ${r.cells.join(" | ")} |`);
239
+ return lines.join("\n");
240
+ }
241
+
242
+ // The References table in an emitted document: from the Section 21 heading to
243
+ // the next heading of the same level or the end of the file.
244
+ function extractReferencesBlock(doc) {
245
+ const lines = doc.split(/\r?\n/);
246
+ const start = lines.findIndex((l) => /^##\s+\d+\.\s+(Referanslar|References)\b/.test(l));
247
+ if (start === -1) return null;
248
+ const out = [];
249
+ for (let i = start + 1; i < lines.length; i++) {
250
+ if (/^##\s+\d+\./.test(lines[i])) break;
251
+ out.push(lines[i]);
252
+ }
253
+ return out.join("\n");
254
+ }
255
+
256
+ // The header row and the separator row carry no source to verify. Matching them
257
+ // against the evidence record is how a correct table gets reported as carrying
258
+ // an invented reference.
259
+ const HEADER_CELLS = new Set(
260
+ Object.values(HEADERS)
261
+ .flat()
262
+ .map((h) => h.toLowerCase()),
263
+ );
264
+
265
+ function tableLocators(block) {
266
+ const found = [];
267
+ for (const line of block.split(/\r?\n/)) {
268
+ if (!line.trim().startsWith("|")) continue;
269
+ const cells = line.split("|").slice(1, -1).map((c) => c.trim());
270
+ if (cells.length < 3) continue;
271
+ if (/^:?-{3,}:?$/.test(cells[0])) continue;
272
+ const locator = cells[2];
273
+ const source = cells[1];
274
+ if (!locator) continue;
275
+ if (HEADER_CELLS.has(locator.toLowerCase()) && HEADER_CELLS.has(cells[0].toLowerCase())) continue;
276
+ found.push({ locator, source, raw: line });
277
+ }
278
+ return found;
279
+ }
280
+
281
+ function check(spec, docPath, lang) {
282
+ const doc = readFileSync(docPath, "utf8");
283
+ const block = extractReferencesBlock(doc);
284
+ const problems = [];
285
+
286
+ if (block === null) {
287
+ problems.push("ERROR: no References section found; Section 21 is never omitted (Locked 21)");
288
+ return problems;
289
+ }
290
+
291
+ const expected = rowsFrom(spec, lang).filter((r) => r.key !== null);
292
+ const rendered = tableLocators(block);
293
+ const renderedText = block;
294
+
295
+ for (const row of expected) {
296
+ if (!renderedText.includes(row.key)) {
297
+ problems.push(
298
+ `ERROR: ${row.type} source consumed by the run but absent from References: ${row.key}`,
299
+ );
300
+ }
301
+ }
302
+
303
+ const expectedKeys = new Set(expected.map((r) => r.key));
304
+ for (const r of rendered) {
305
+ // Header rows and placeholder cells carry no locator to verify.
306
+ if (r.locator === NONE || r.locator === "" || /^<.*>$/.test(r.locator)) continue;
307
+ const known = [...expectedKeys].some((k) => r.locator.includes(k) || k.includes(r.locator));
308
+ if (!known) {
309
+ problems.push(
310
+ `ERROR: References row has no matching evidence entry (invented reference): ${r.locator}`,
311
+ );
312
+ }
313
+ }
314
+
315
+ return problems;
316
+ }
317
+
318
+ function main() {
319
+ const argv = process.argv.slice(2);
320
+ const statePath = argv[0];
321
+ if (!statePath || statePath.startsWith("--")) {
322
+ process.stderr.write(
323
+ "usage: build-references.mjs <state.json|-> [--lang tr|en] [--check <doc.md>]\n",
324
+ );
325
+ process.exit(2);
326
+ }
327
+
328
+ let spec;
329
+ try {
330
+ spec = readState(statePath);
331
+ } catch (err) {
332
+ process.stderr.write(`ERROR: cannot read state: ${err.message}\n`);
333
+ process.exit(2);
334
+ }
335
+
336
+ const lang = argOf(argv, "--lang") ?? spec.language ?? "tr";
337
+ if (!HEADERS[lang]) {
338
+ process.stderr.write(`ERROR: unknown language "${lang}"; expected tr or en\n`);
339
+ process.exit(2);
340
+ }
341
+
342
+ const docPath = argOf(argv, "--check");
343
+ if (docPath) {
344
+ const problems = check(spec, docPath, lang);
345
+ if (problems.length) {
346
+ for (const p of problems) process.stderr.write(`${p}\n`);
347
+ process.stderr.write(`\n${problems.length} references coverage failure(s)\n`);
348
+ process.exit(1);
349
+ }
350
+ process.stdout.write("references coverage ok\n");
351
+ process.exit(0);
352
+ }
353
+
354
+ const rows = rowsFrom(spec, lang);
355
+ if (rows.length === 0) {
356
+ // An analysis with no sources at all is possible (a pure free-text run) but
357
+ // it should say so rather than emit an empty table with a header.
358
+ process.stdout.write(
359
+ lang === "tr"
360
+ ? "Bu koşuda getirilen kaynak yok.\n"
361
+ : "No sources were fetched for this run.\n",
362
+ );
363
+ process.exit(0);
364
+ }
365
+ process.stdout.write(`${renderTable(rows, lang)}\n`);
366
+ }
367
+
368
+ main();