forgedock 1.0.22 → 1.1.2

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 (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge-protocol-conformance — FORGE Annotation Protocol conformance test runner.
4
+ *
5
+ * Usage:
6
+ * forge-protocol-conformance [fixtures-dir]
7
+ * node src/cli.js [fixtures-dir]
8
+ *
9
+ * Reads fixtures from the given directory (default: ./fixtures/).
10
+ * Each fixture is a JSON file with:
11
+ * {
12
+ * "description": "human-readable test name",
13
+ * "input": "comment body string to parse",
14
+ * "expect": {
15
+ * "type": "INVESTIGATOR",
16
+ * "valid": true,
17
+ * "sentinelState": "complete",
18
+ * "inlineValue": null,
19
+ * "fieldChecks": { "Verdict": "CONFIRMED" } // optional
20
+ * }
21
+ * }
22
+ *
23
+ * Exit code: 0 if all fixtures pass, 1 if any fail.
24
+ *
25
+ * @license MIT
26
+ */
27
+
28
+ import { readFileSync, readdirSync } from 'node:fs';
29
+ import { resolve, extname } from 'node:path';
30
+ import { parse } from './parse.js';
31
+ import { validate } from './validate.js';
32
+
33
+ const fixturesDir = resolve(process.argv[2] ?? 'fixtures');
34
+
35
+ let passed = 0;
36
+ let failed = 0;
37
+ const failures = [];
38
+
39
+ let files;
40
+ try {
41
+ files = readdirSync(fixturesDir).filter(f => extname(f) === '.json').sort();
42
+ } catch (err) {
43
+ console.error(`Cannot read fixtures directory: ${fixturesDir}`);
44
+ console.error(err.message);
45
+ process.exit(1);
46
+ }
47
+
48
+ if (files.length === 0) {
49
+ console.error(`No .json fixtures found in ${fixturesDir}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ for (const file of files) {
54
+ const fixturePath = resolve(fixturesDir, file);
55
+ let fixture;
56
+ try {
57
+ fixture = JSON.parse(readFileSync(fixturePath, 'utf8'));
58
+ } catch (err) {
59
+ console.error(`SKIP ${file} — cannot parse JSON: ${err.message}`);
60
+ continue;
61
+ }
62
+
63
+ const { description, input, expect: expected } = fixture;
64
+ const label = `${file}: ${description ?? '(no description)'}`;
65
+
66
+ if (typeof input !== 'string') {
67
+ console.error(`SKIP ${label} — "input" must be a string`);
68
+ continue;
69
+ }
70
+
71
+ const annotations = parse(input);
72
+
73
+ // Find the annotation matching the expected type
74
+ const ann = annotations.find(a => a.type === expected.type);
75
+
76
+ const fixtureErrors = [];
77
+
78
+ if (!ann) {
79
+ fixtureErrors.push(`Expected annotation type "${expected.type}" not found in parsed output`);
80
+ } else {
81
+ // Sentinel state check
82
+ if (expected.sentinelState !== undefined && ann.sentinelState !== expected.sentinelState) {
83
+ fixtureErrors.push(
84
+ `sentinelState: expected "${expected.sentinelState}", got "${ann.sentinelState}"`,
85
+ );
86
+ }
87
+
88
+ // Inline value check
89
+ if ('inlineValue' in expected) {
90
+ if (ann.inlineValue !== expected.inlineValue) {
91
+ fixtureErrors.push(
92
+ `inlineValue: expected ${JSON.stringify(expected.inlineValue)}, got ${JSON.stringify(ann.inlineValue)}`,
93
+ );
94
+ }
95
+ }
96
+
97
+ // Field checks
98
+ if (expected.fieldChecks) {
99
+ for (const [key, val] of Object.entries(expected.fieldChecks)) {
100
+ if (ann.fields[key] !== val) {
101
+ fixtureErrors.push(
102
+ `field "${key}": expected "${val}", got "${ann.fields[key] ?? '(missing)'}"`,
103
+ );
104
+ }
105
+ }
106
+ }
107
+
108
+ // validate() result check
109
+ const { valid, errors } = validate(ann);
110
+ if (expected.valid === true && !valid) {
111
+ fixtureErrors.push(`Expected valid=true but got errors: ${errors.join('; ')}`);
112
+ } else if (expected.valid === false && valid) {
113
+ fixtureErrors.push(`Expected valid=false but annotation passed validation`);
114
+ }
115
+ }
116
+
117
+ if (fixtureErrors.length === 0) {
118
+ console.log(`ok ${label}`);
119
+ passed++;
120
+ } else {
121
+ console.log(`FAIL ${label}`);
122
+ for (const e of fixtureErrors) {
123
+ console.log(` ${e}`);
124
+ }
125
+ failures.push(label);
126
+ failed++;
127
+ }
128
+ }
129
+
130
+ console.log('');
131
+ console.log(`Results: ${passed} passed, ${failed} failed (${files.length} fixtures total)`);
132
+
133
+ if (failed > 0) {
134
+ process.exit(1);
135
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * FORGE Annotation Protocol v1.0 — emitter.
3
+ *
4
+ * emit(type, fields) → well-formed annotation string
5
+ *
6
+ * Produces the canonical text for a FORGE annotation including:
7
+ * - Opening tag (§3.1)
8
+ * - Inline-value form (§3.4) when the type uses it
9
+ * - Completion sentinel (§3.2) when the type defines one
10
+ *
11
+ * @license MIT
12
+ */
13
+
14
+ import { RESERVED_TYPES, RESERVED_TYPE_NAMES } from './types.js';
15
+
16
+ // A folded field/inline value that is itself (whole, trimmed) shaped like a FORGE
17
+ // opening tag or a generic sentinel line has no legitimate reason to look that way —
18
+ // parse()'s line-anchored matching (see parse.js OPENING_TAG_RE / resolveSentinelState)
19
+ // treats any line matching this shape as live protocol structure. Untrusted text
20
+ // (issue titles, branch names, freeform error/output strings) flowing into emit()
21
+ // must never be able to forge one. (forge#1594)
22
+ const FORGE_TAG_LINE_RE =
23
+ /^\s*<!--\s*(?:FORGE:[A-Z_]+(?::.*)?|[A-Z_][A-Z_:]*:(?:COMPLETE|PARTIAL))\s*-->\s*$/;
24
+
25
+ // Newline variants that would let a value spill onto its own line(s).
26
+ const NEWLINE_RE = /\r\n|\r|\n/g;
27
+
28
+ /**
29
+ * Sanitize a value before it is serialized into a `**Key**: value` field line or
30
+ * an inline-value tag. Two independent protections:
31
+ *
32
+ * 1. Newlines are folded to a single space so a multi-line value can never
33
+ * produce a new line of its own — every real FORGE tag (opening tag,
34
+ * sentinel, control marker) occupies a whole line, so folding denies an
35
+ * injected value the only shape parse() treats as structural.
36
+ * 2. HTML comment delimiters (`<!--`, `-->`, and `--!>`) are escaped so a
37
+ * value can never open a new unterminated HTML comment or prematurely close
38
+ * the enclosing `<!-- ... -->` comment — GitHub's renderer treats a literal
39
+ * `<!--` as the start of a new (nested-looking) comment, visually swallowing
40
+ * subsequent lines including the completion sentinel, and treats a literal
41
+ * `-->` as ending the enclosing comment early, leaking the remainder as
42
+ * visible rendered text. Both directions of the delimiter pair must be
43
+ * escaped. (forge#1594, forge#1638)
44
+ *
45
+ * As defense in depth, if a value still folds down to a line that is itself a
46
+ * bare FORGE tag/sentinel shape, emit() rejects it outright rather than silently
47
+ * emitting something a parser could mistake for real protocol structure.
48
+ *
49
+ * @param {*} rawValue
50
+ * @returns {string}
51
+ */
52
+ function sanitizeFieldValue(rawValue) {
53
+ const folded = String(rawValue).replace(NEWLINE_RE, ' ');
54
+ if (FORGE_TAG_LINE_RE.test(folded.trim())) {
55
+ throw new TypeError(
56
+ `emit(): field value resolves to a bare FORGE tag/sentinel line, which is not permitted: ${JSON.stringify(rawValue)}`,
57
+ );
58
+ }
59
+ // Escape the HTML comment opener ("<!--") so a value cannot start a new
60
+ // unterminated HTML comment in GitHub's renderer, which would visually swallow
61
+ // subsequent lines — the same rendering-leak the closer escaping below prevents
62
+ // from the opposite direction. (forge#1638)
63
+ const withOpenerEscaped = folded.replace(/<!--/g, '&lt;!--');
64
+ // Escape both HTML comment-close forms ("-->" and "--!>") — only replacing the
65
+ // literal character sequence (not just documenting the risk) prevents the
66
+ // enclosing comment from terminating early. (forge#1594; noted but never fixed
67
+ // for the FORGE:STATE codec in bin/engine/state.mjs — see issue investigation)
68
+ return withOpenerEscaped.replace(/--(!)?>/g, (_, bang) => `--${bang || ''}&gt;`);
69
+ }
70
+
71
+ /**
72
+ * Emit a well-formed FORGE annotation string.
73
+ *
74
+ * @param {string} type - Annotation type (e.g. "INVESTIGATOR", "KNOWLEDGE_GIST")
75
+ * @param {Object} [fields={}] - Key/value pairs to include as **Key**: value lines,
76
+ * OR for inline-value types, pass { value: 'https://...' }
77
+ * @returns {string}
78
+ */
79
+ export function emit(type, fields = {}) {
80
+ if (typeof type !== 'string' || !type) {
81
+ throw new TypeError('emit() requires a non-empty type string');
82
+ }
83
+
84
+ const upperType = type.toUpperCase();
85
+ const typeDef = RESERVED_TYPES[upperType];
86
+
87
+ // Inline-value form (§3.4)
88
+ if (typeDef?.inlineValue) {
89
+ const value = fields.value ?? '';
90
+ return `<!-- FORGE:${upperType}: ${sanitizeFieldValue(value)} -->`;
91
+ }
92
+
93
+ // Control marker form (§4.3) — just the tag, no body
94
+ if (typeDef?.controlMarker) {
95
+ return `<!-- FORGE:${upperType} -->`;
96
+ }
97
+
98
+ // Full annotation form
99
+ const lines = [`<!-- FORGE:${upperType} -->`];
100
+
101
+ // Emit each field as a **Key**: value line
102
+ // Both key and val are sanitized — a partial-sanitization hole where only the
103
+ // value was sanitized was identified in forge#1637. Keys can carry the same
104
+ // newline / comment-terminator injection vectors as values.
105
+ const fieldEntries = Object.entries(fields);
106
+ if (fieldEntries.length > 0) {
107
+ for (const [key, val] of fieldEntries) {
108
+ lines.push(`**${sanitizeFieldValue(key)}**: ${sanitizeFieldValue(val)}`);
109
+ }
110
+ }
111
+
112
+ // Completion sentinel (§3.2)
113
+ if (typeDef?.completionSentinel) {
114
+ lines.push(`<!-- ${typeDef.completionSentinel} -->`);
115
+ }
116
+
117
+ return lines.join('\n');
118
+ }
119
+
120
+ /**
121
+ * Emit a partial sentinel for an annotation that was interrupted.
122
+ * Spec §3.3: producers MAY mark interrupted annotations partial.
123
+ *
124
+ * @param {string} type
125
+ * @returns {string}
126
+ */
127
+ export function emitPartial(type) {
128
+ const upperType = type.toUpperCase();
129
+ const typeDef = RESERVED_TYPES[upperType];
130
+
131
+ if (typeDef?.partialSentinel) {
132
+ return `<!-- ${typeDef.partialSentinel} -->`;
133
+ }
134
+ // Fallback generic form
135
+ return `<!-- ${upperType}:PARTIAL -->`;
136
+ }
137
+
138
+ /**
139
+ * Check if a type is known/reserved.
140
+ *
141
+ * @param {string} type
142
+ * @returns {boolean}
143
+ */
144
+ export function isKnownType(type) {
145
+ return RESERVED_TYPE_NAMES.has(typeof type === 'string' ? type.toUpperCase() : type);
146
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @forgedock/protocol — FORGE Annotation Protocol v1.0 reference implementation.
3
+ *
4
+ * Provides parse, validate, and emit for all 13 reserved annotation types
5
+ * defined in the FORGE Annotation Protocol specification (CC-BY-4.0).
6
+ *
7
+ * @example
8
+ * import { parse, validate, emit } from '@forgedock/protocol';
9
+ *
10
+ * // Parse annotations from a GitHub issue/PR comment body
11
+ * const annotations = parse(commentBody);
12
+ *
13
+ * // Validate a parsed annotation
14
+ * const { valid, errors } = validate(annotations[0]);
15
+ *
16
+ * // Emit a well-formed annotation string
17
+ * const text = emit('INVESTIGATOR', {
18
+ * Verdict: 'CONFIRMED',
19
+ * Confidence: 'HIGH',
20
+ * Severity: 'MEDIUM',
21
+ * 'Task Type': 'Bug Fix',
22
+ * 'Decomposition Assessment': 'NO — single-file fix',
23
+ * });
24
+ *
25
+ * @license MIT
26
+ */
27
+
28
+ export { parse } from './parse.js';
29
+ export { validate } from './validate.js';
30
+ export { emit, emitPartial, isKnownType } from './emit.js';
31
+ export { RESERVED_TYPES, RESERVED_TYPE_NAMES, Category, SentinelState } from './types.js';
32
+
33
+ /**
34
+ * @typedef {Object} ParsedAnnotation
35
+ * @property {string} type - Annotation type identifier (e.g. "INVESTIGATOR")
36
+ * @property {string} raw - Full annotation text including opening tag
37
+ * @property {string} body - Markdown body (lines after opening tag)
38
+ * @property {string|null} inlineValue - Value for inline-value form, null otherwise
39
+ * @property {import('./types.js').SentinelState} sentinelState - complete/partial/interrupted
40
+ * @property {boolean} isReserved - True if type is in the reserved set
41
+ * @property {boolean} isControl - True if type is a control/error marker
42
+ * @property {Object.<string,string>} fields - Extracted **Key**: value pairs
43
+ */
@@ -0,0 +1,229 @@
1
+ /**
2
+ * FORGE Annotation Protocol v1.0 — parser.
3
+ *
4
+ * parse(commentBody) → Array<ParsedAnnotation>
5
+ *
6
+ * Each ParsedAnnotation has:
7
+ * type {string} annotation type identifier (e.g. "INVESTIGATOR")
8
+ * raw {string} the full text of the annotation (opening tag + body)
9
+ * body {string} Markdown body (everything after the opening tag line)
10
+ * inlineValue {string|null} value for inline-value form (§3.4), null otherwise
11
+ * sentinelState {SentinelState} complete / partial / interrupted
12
+ * isReserved {boolean} true if type is in the reserved set (§4)
13
+ * isControl {boolean} true if type is a control/error marker (§4.3)
14
+ * fields {Object} key/value pairs extracted from bold-header lines
15
+ *
16
+ * @license MIT
17
+ */
18
+
19
+ import { RESERVED_TYPES, RESERVED_TYPE_NAMES, SentinelState } from './types.js';
20
+
21
+ // Matches: <!-- FORGE:TYPE --> or <!-- FORGE:TYPE: value -->
22
+ // Anchored to require the tag to be the ENTIRE (trimmed) line — a FORGE-tag-shaped
23
+ // substring embedded inside other line content (e.g. a **Commits**: field value that
24
+ // happens to mention a FORGE tag) is never a structural annotation opener. Every real
25
+ // FORGE tag emission (opening tags and sentinel markers alike) occupies its own line;
26
+ // this anchor makes "is the whole line" the structural test instead of a bare substring
27
+ // search, which previously let field-value text split/corrupt annotations. (forge#1524)
28
+ const OPENING_TAG_RE = /^\s*<!--\s*FORGE:([A-Z_]+)(?::\s*(.*?))?\s*-->\s*$/;
29
+
30
+ // Sentinel suffixes that appear as inline-values in tags that look like opening tags
31
+ // but are actually completion/partial markers, e.g. <!-- FORGE:CONTEXT:COMPLETE -->
32
+ const SENTINEL_SUFFIXES = new Set(['COMPLETE', 'PARTIAL']);
33
+
34
+ // Matches bold-key lines: **Key**: value (used for field extraction)
35
+ const BOLD_FIELD_RE = /^\*\*([^*]+)\*\*\s*:\s*(.+)$/;
36
+
37
+ // Generic (unknown-type) sentinel patterns, anchored to a whole line — see
38
+ // resolveSentinelState() below for why anchoring matters. (forge#1594)
39
+ const GENERIC_COMPLETE_RE = /^\s*<!--\s*[A-Z_:]+:COMPLETE\s*-->\s*$/m;
40
+ const GENERIC_PARTIAL_RE = /^\s*<!--\s*[A-Z_:]+:PARTIAL\s*-->\s*$/m;
41
+
42
+ /**
43
+ * Determine sentinel state for an annotation body given its type definition.
44
+ *
45
+ * @param {string} body
46
+ * @param {object|undefined} typeDef
47
+ * @returns {SentinelState}
48
+ */
49
+ function resolveSentinelState(body, typeDef) {
50
+ if (!typeDef) {
51
+ // Unknown type — look for generic sentinel patterns. Anchored to require the
52
+ // sentinel to occupy its own (trimmed) line — the same structural test as
53
+ // OPENING_TAG_RE above. A field value that merely *mentions* sentinel-shaped
54
+ // text (e.g. **Commits**: ... <!-- FORGE:BUILDER:COMPLETE -->) must not be
55
+ // able to spoof completion; only a sentinel on its own line is real. (forge#1594)
56
+ if (GENERIC_COMPLETE_RE.test(body)) return SentinelState.COMPLETE;
57
+ if (GENERIC_PARTIAL_RE.test(body)) return SentinelState.PARTIAL;
58
+ return SentinelState.INTERRUPTED;
59
+ }
60
+
61
+ if (typeDef.controlMarker) {
62
+ // Control markers carry no body; their presence is the signal — treat as complete
63
+ return SentinelState.COMPLETE;
64
+ }
65
+
66
+ if (!typeDef.completionSentinel) {
67
+ // Types without a defined sentinel (CONTRACT, REVIEWER, TRAJECTORY) are
68
+ // always treated as complete once they exist — there is no incompleteness signal.
69
+ return SentinelState.COMPLETE;
70
+ }
71
+
72
+ // Anchored to a whole line (with the `m` flag so `^`/`$` match at every line
73
+ // boundary within body, not just start/end of the whole string) — a real
74
+ // sentinel always occupies its own line (see emit.js, which always emits it
75
+ // that way). This is the sentinel-detection mirror of the OPENING_TAG_RE
76
+ // anchor above: an unanchored substring test previously let a field value
77
+ // that merely *contains* the sentinel text (e.g. a **Commits** message quoting
78
+ // it) spoof completion for an annotation that was never actually completed.
79
+ // (forge#1594 — inverse of the #1524 opening-tag anchor gap)
80
+ const completionPattern = new RegExp(
81
+ `^\\s*<!--\\s*${escapeRegExp(typeDef.completionSentinel)}\\s*-->\\s*$`,
82
+ 'm',
83
+ );
84
+ if (completionPattern.test(body)) return SentinelState.COMPLETE;
85
+
86
+ if (typeDef.partialSentinel) {
87
+ const partialPattern = new RegExp(
88
+ `^\\s*<!--\\s*${escapeRegExp(typeDef.partialSentinel)}\\s*-->\\s*$`,
89
+ 'm',
90
+ );
91
+ if (partialPattern.test(body)) return SentinelState.PARTIAL;
92
+ }
93
+
94
+ return SentinelState.INTERRUPTED;
95
+ }
96
+
97
+ function escapeRegExp(str) {
98
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
99
+ }
100
+
101
+ /**
102
+ * Extract **Key**: value fields from an annotation body.
103
+ *
104
+ * @param {string} body
105
+ * @returns {Object.<string,string>}
106
+ */
107
+ function extractFields(body) {
108
+ const fields = {};
109
+ for (const line of body.split('\n')) {
110
+ const m = line.trim().match(BOLD_FIELD_RE);
111
+ if (m) {
112
+ fields[m[1].trim()] = m[2].trim();
113
+ }
114
+ }
115
+ return fields;
116
+ }
117
+
118
+ /**
119
+ * Parse all FORGE annotations from a comment body.
120
+ *
121
+ * Spec §3.1: an annotation ends at the next <!-- FORGE: tag or end of comment.
122
+ * Spec §3.4: inline-value form — value is extracted directly from the tag.
123
+ * Spec §7.2.4: unknown types are tolerated; never throw on them.
124
+ *
125
+ * @param {string} commentBody
126
+ * @returns {Array<import('./index.js').ParsedAnnotation>}
127
+ */
128
+ export function parse(commentBody) {
129
+ if (typeof commentBody !== 'string') {
130
+ throw new TypeError(`parse() expects a string, got ${typeof commentBody}`);
131
+ }
132
+
133
+ const lines = commentBody.split('\n');
134
+ const annotations = [];
135
+
136
+ let i = 0;
137
+ while (i < lines.length) {
138
+ const line = lines[i];
139
+ const tagMatch = line.match(OPENING_TAG_RE);
140
+
141
+ if (!tagMatch) {
142
+ i++;
143
+ continue;
144
+ }
145
+
146
+ const type = tagMatch[1];
147
+ const rawInlineValue = tagMatch[2] !== undefined ? tagMatch[2].trim() : null;
148
+ const typeDefForTag = RESERVED_TYPES[type];
149
+
150
+ // Sentinel lines like <!-- FORGE:CONTEXT:COMPLETE --> match the opening tag regex
151
+ // because the regex captures "CONTEXT" as type and "COMPLETE" as inlineValue.
152
+ // These are NOT new annotations — they are closing markers and must stay in
153
+ // the preceding annotation's body. The inner body-accumulation loop handles
154
+ // this by including such lines rather than breaking on them. However, if a
155
+ // sentinel line appears at the very beginning (before any annotation), skip it.
156
+ //
157
+ // Exception: types that declare inlineValue: true (KNOWLEDGE_GIST, MILESTONE_INDEX,
158
+ // PRIOR_GIST) use COMPLETE/PARTIAL as legitimate inline values, not sentinel markers
159
+ // — e.g. <!-- FORGE:PRIOR_GIST: COMPLETE --> is a real annotation whose value is the
160
+ // string "COMPLETE". Only guess "sentinel" for types that don't use inline-value form
161
+ // (typeDefForTag?.inlineValue is false/undefined for all of those, including unknown
162
+ // types where typeDefForTag itself is undefined). (forge#1526)
163
+ if (
164
+ rawInlineValue !== null &&
165
+ SENTINEL_SUFFIXES.has(rawInlineValue) &&
166
+ !typeDefForTag?.inlineValue
167
+ ) {
168
+ i++;
169
+ continue;
170
+ }
171
+
172
+ const inlineValue = rawInlineValue;
173
+ const typeDef = typeDefForTag;
174
+ const isControl = typeDef?.controlMarker === true;
175
+ const isReserved = RESERVED_TYPE_NAMES.has(type);
176
+
177
+ // Collect body lines until the next true annotation opening tag or end of comment.
178
+ // Sentinel lines like <!-- FORGE:CONTEXT:COMPLETE --> match OPENING_TAG_RE but are
179
+ // closing markers, not new annotation openings — include them in the body.
180
+ const bodyLines = [];
181
+ let j = i + 1;
182
+ while (j < lines.length) {
183
+ const bodyLineMatch = lines[j].match(OPENING_TAG_RE);
184
+ if (bodyLineMatch) {
185
+ const bodyLineType = bodyLineMatch[1];
186
+ const bodyLineInlineValue = bodyLineMatch[2] !== undefined ? bodyLineMatch[2].trim() : null;
187
+ const bodyLineTypeDef = RESERVED_TYPES[bodyLineType];
188
+ // Sentinel-like tags (COMPLETE/PARTIAL inline value) → include in body, don't break.
189
+ // Exception: if bodyLineType is an inline-value type (KNOWLEDGE_GIST, MILESTONE_INDEX,
190
+ // PRIOR_GIST), COMPLETE/PARTIAL is a genuine inline value for a NEW annotation, not a
191
+ // sentinel — fall through to "True opening tag" below so it starts its own annotation
192
+ // instead of being folded into the preceding one's body. (forge#1526)
193
+ if (
194
+ bodyLineInlineValue !== null &&
195
+ SENTINEL_SUFFIXES.has(bodyLineInlineValue) &&
196
+ !bodyLineTypeDef?.inlineValue
197
+ ) {
198
+ bodyLines.push(lines[j]);
199
+ j++;
200
+ continue;
201
+ }
202
+ // True opening tag → end this annotation's body
203
+ break;
204
+ }
205
+ bodyLines.push(lines[j]);
206
+ j++;
207
+ }
208
+
209
+ const body = bodyLines.join('\n');
210
+ const raw = line + (body.length > 0 ? '\n' + body : '');
211
+ const sentinelState = resolveSentinelState(body, typeDef);
212
+ const fields = inlineValue !== null ? {} : extractFields(body);
213
+
214
+ annotations.push({
215
+ type,
216
+ raw,
217
+ body,
218
+ inlineValue,
219
+ sentinelState,
220
+ isReserved,
221
+ isControl,
222
+ fields,
223
+ });
224
+
225
+ i = j;
226
+ }
227
+
228
+ return annotations;
229
+ }