@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.
- package/CHANGELOG.md +46 -0
- package/README.md +4 -4
- package/README.tr.md +4 -4
- package/docs/architecture.md +2 -2
- package/docs/ecosystem.md +5 -5
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +18 -5
- package/pipeline/commands/multi-agent/feedback/SKILL.md +51 -0
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +32 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +20 -18
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/multi-agent-refs/analysis/intake.md +30 -1
- package/pipeline/multi-agent-refs/analysis/locked.md +11 -6
- package/pipeline/multi-agent-refs/analysis/render.md +20 -5
- package/pipeline/multi-agent-refs/analysis/review.md +86 -0
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +436 -0
- package/pipeline/multi-agent-refs/analysis-template.md +31 -13
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -7
- package/pipeline/multi-agent-refs/website-deploy.md +87 -0
- package/pipeline/schemas/analysis-spec.schema.json +21 -1
- package/pipeline/schemas/prefs.schema.json +41 -0
- package/pipeline/scripts/build-references.mjs +368 -0
- package/pipeline/scripts/feedback-send.mjs +181 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +130 -9
- package/pipeline/scripts/website-deploy-commit.sh +102 -0
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +18 -2
- package/pipeline/skills/shared/core/multi-agent-feedback/SKILL.md +30 -0
- package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +31 -0
- 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
|
|
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
|
|
922
|
-
|
|
923
|
-
|
|
|
924
|
-
|
|
|
925
|
-
|
|
|
926
|
-
|
|
|
927
|
-
|
|
|
928
|
-
|
|
|
929
|
-
|
|
|
930
|
-
|
|
|
931
|
-
|
|
|
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 (
|
|
9
|
+
## 1. Command Inventory (53 files, 49 live commands)
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
analysis, analysis-resolve, autopilot, build-optimize, channels,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
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();
|