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.
- package/README.md +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- 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, '<!--');
|
|
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 || ''}>`);
|
|
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
|
+
}
|