forgedock 1.1.1 → 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 +53 -8
- package/bin/batch-runner.mjs +339 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +20 -0
- package/bin/engine/state.mjs +8 -3
- package/bin/engine-cli.mjs +177 -5
- package/bin/forgedock.mjs +409 -26
- 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 +72 -14
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +68 -19
- package/bin/settings-hook.mjs +235 -1
- 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 +80 -8
- 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 +58 -1
- package/commands/design-bench.md +5 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +121 -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 +23 -2
- 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 -2045
- 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 +248 -10
- 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 +591 -52
- 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 +3 -1
- package/commands/work-on/build/validate.md +26 -7
- package/commands/work-on/build.md +163 -24
- package/commands/work-on/close.md +130 -3
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +24 -4
- package/commands/work-on-monolithic.md +7 -2
- package/commands/work-on.md +202 -4
- package/docs/BRAND.md +14 -7
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +5 -8
- 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 +43 -4
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +9 -1
- 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/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 +3 -1
- 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/worktree-lifecycle.sh +3 -3
- package/templates/devdocs/agent/using-forgedock.md +14 -0
- package/docs/site/.vitepress/config.mjs +0 -85
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "docs(api): add complete API reference covering all endpoints"
|
|
3
|
+
labels: ["documentation", "priority:P3"]
|
|
4
|
+
difficulty: "easy"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
The demo API now has 9 endpoints (health, list, count, tags, get, create, patch,
|
|
10
|
+
delete, and the bulk/archive/restore endpoints added by later issues) but only
|
|
11
|
+
the original 5 are described in issue #5's `docs/API.md` — and issue #5 may not
|
|
12
|
+
have been run yet. Either way, the current baseline has no `docs/API.md`, or an
|
|
13
|
+
incomplete one.
|
|
14
|
+
|
|
15
|
+
## Proposed Solution
|
|
16
|
+
|
|
17
|
+
Create (or rewrite) `docs/API.md` documenting all endpoints in the **current
|
|
18
|
+
baseline** (i.e. after the source-code changes in this PR land):
|
|
19
|
+
|
|
20
|
+
| Method | Path | Auth | Description |
|
|
21
|
+
|--------|------|------|-------------|
|
|
22
|
+
| GET | `/health` | no | Liveness check |
|
|
23
|
+
| GET | `/notes` | no | List notes (supports `?where=`, `?tag=`, `?limit=`, `?offset=`) |
|
|
24
|
+
| GET | `/notes/count` | no | Count notes (supports `?owner=`, `?secret=`) |
|
|
25
|
+
| GET | `/notes/tags` | no | List all unique tags |
|
|
26
|
+
| GET | `/notes/:id` | no | Fetch one note |
|
|
27
|
+
| POST | `/notes` | yes | Create a note |
|
|
28
|
+
| PATCH | `/notes/:id` | yes | Update title/body |
|
|
29
|
+
| DELETE | `/notes/:id` | no* | Delete a note |
|
|
30
|
+
|
|
31
|
+
Include a `curl` example for each endpoint, note the intentional flaws inline
|
|
32
|
+
(e.g. `?where=` is injection-vulnerable — see issue #2), and describe the
|
|
33
|
+
`Authorization: Bearer demo-token` header.
|
|
34
|
+
|
|
35
|
+
`*` DELETE is intentionally unprotected — document this as a known flaw (issue #1).
|
|
36
|
+
|
|
37
|
+
## Affected Files
|
|
38
|
+
|
|
39
|
+
1. `docs/API.md` (NEW or REWRITE) — full endpoint reference.
|
|
40
|
+
2. `README.md` — add a "API reference: docs/API.md" link in the "What's inside"
|
|
41
|
+
section if not already present.
|
|
42
|
+
|
|
43
|
+
## Acceptance Criteria
|
|
44
|
+
|
|
45
|
+
- [ ] `docs/API.md` documents all 8 baseline endpoints with method, path, auth
|
|
46
|
+
requirement, description, and a `curl` example.
|
|
47
|
+
- [ ] The intentional flaws (injection, missing auth on DELETE) are called out
|
|
48
|
+
inline with references to the corresponding issue numbers.
|
|
49
|
+
- [ ] `README.md` links to `docs/API.md`.
|
|
50
|
+
- [ ] No code changes required.
|
|
51
|
+
|
|
52
|
+
> Non-code work that still goes through the full pipeline — investigate the
|
|
53
|
+
> current route surface, write accurate docs, review for completeness.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "docs(demo): document the intentional-flaw and graded-issue authoring convention"
|
|
3
|
+
labels: ["documentation", "priority:P3"]
|
|
4
|
+
difficulty: "easy"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
The demo corpus relies on a deliberate convention — intentional flaws in source
|
|
10
|
+
code, graded issue difficulty, DEMO NOTE comments — but this convention is
|
|
11
|
+
undocumented outside of the source code comments themselves. Someone contributing
|
|
12
|
+
new issues, or using this corpus as a template for their own eval harness, has no
|
|
13
|
+
single reference explaining how the pieces fit together.
|
|
14
|
+
|
|
15
|
+
## Proposed Solution
|
|
16
|
+
|
|
17
|
+
Create `docs/CORPUS-AUTHORING.md` that explains:
|
|
18
|
+
|
|
19
|
+
1. **The intentional-flaw pattern** — why flaws are introduced in baseline
|
|
20
|
+
source code and how to mark them with `>>> DEMO NOTE:` comments.
|
|
21
|
+
2. **The graded-difficulty taxonomy** — the five categories (bug, feature,
|
|
22
|
+
refactor, perf, docs) and the three difficulty tiers (easy, medium, hard),
|
|
23
|
+
with one example per category from this corpus.
|
|
24
|
+
3. **Issue spec format** — the required YAML frontmatter fields (`title`,
|
|
25
|
+
`labels`, `difficulty`), the required markdown sections (`## Problem`,
|
|
26
|
+
`## Affected Files`, `## Acceptance Criteria`), and the closing `> ...`
|
|
27
|
+
callout paragraph.
|
|
28
|
+
4. **Independence constraint** — how to verify that a new issue is resolvable
|
|
29
|
+
against the baseline commit without depending on other unmerged issues.
|
|
30
|
+
|
|
31
|
+
## Affected Files
|
|
32
|
+
|
|
33
|
+
1. `docs/CORPUS-AUTHORING.md` (NEW).
|
|
34
|
+
2. `README.md` — add a link to `docs/CORPUS-AUTHORING.md` in a "Contributing
|
|
35
|
+
issues" sub-section.
|
|
36
|
+
|
|
37
|
+
## Acceptance Criteria
|
|
38
|
+
|
|
39
|
+
- [ ] `docs/CORPUS-AUTHORING.md` covers all four sections listed above.
|
|
40
|
+
- [ ] Each section references at least one existing issue from the corpus as a
|
|
41
|
+
concrete example.
|
|
42
|
+
- [ ] `README.md` links to the new doc.
|
|
43
|
+
- [ ] No code changes required.
|
|
44
|
+
|
|
45
|
+
> A meta-doc that makes the demo self-explaining. Useful for anyone wanting to
|
|
46
|
+
> extend the corpus or use it as a template for their own benchmark.
|
|
@@ -48,8 +48,25 @@ async function main() {
|
|
|
48
48
|
const unauth = await request(port, 'POST', '/notes', { json: { title: 'nope' } });
|
|
49
49
|
assert.strictEqual(unauth.status, 401, 'POST without token returns 401');
|
|
50
50
|
|
|
51
|
+
const count = await request(port, 'GET', '/notes/count');
|
|
52
|
+
assert.strictEqual(typeof count.body.count, 'number', 'GET /notes/count returns a number');
|
|
53
|
+
|
|
54
|
+
const tags = await request(port, 'GET', '/notes/tags');
|
|
55
|
+
assert.ok(Array.isArray(tags.body.tags), 'GET /notes/tags returns an array');
|
|
56
|
+
|
|
57
|
+
const page = await request(port, 'GET', '/notes?limit=1');
|
|
58
|
+
assert.ok(page.body.notes.length <= 1, 'GET /notes?limit=1 returns at most one note');
|
|
59
|
+
|
|
60
|
+
const noteId = created.body.note.id;
|
|
61
|
+
const patched = await request(port, 'PATCH', `/notes/${noteId}`, {
|
|
62
|
+
token: 'demo-token',
|
|
63
|
+
json: { title: 'smoke (patched)' },
|
|
64
|
+
});
|
|
65
|
+
assert.strictEqual(patched.status, 200, 'PATCH with token returns 200');
|
|
66
|
+
assert.strictEqual(patched.body.note.title, 'smoke (patched)', 'PATCH updates the title');
|
|
67
|
+
|
|
51
68
|
// eslint-disable-next-line no-console
|
|
52
|
-
console.log('SMOKE OK — health, list, create(auth), create(unauth) all pass');
|
|
69
|
+
console.log('SMOKE OK — health, list, create(auth), create(unauth), count, tags, pagination, patch all pass');
|
|
53
70
|
server.close();
|
|
54
71
|
}
|
|
55
72
|
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
// >>> It exists only inside examples/forgedock-demo so the review agents have
|
|
11
11
|
// >>> something to catch. Do not copy this pattern into real code.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
{ id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false },
|
|
15
|
-
{ id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true },
|
|
16
|
-
{ id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false },
|
|
13
|
+
const SEED = [
|
|
14
|
+
{ id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false, tags: ['errand'], archived: false, createdAt: '2024-01-01T00:00:00.000Z' },
|
|
15
|
+
{ id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true, tags: ['work'], archived: false, createdAt: '2024-01-02T00:00:00.000Z' },
|
|
16
|
+
{ id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false, tags: ['fitness', 'errand'], archived: false, createdAt: '2024-01-03T00:00:00.000Z' },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
+
let notes = SEED.map((n) => ({ ...n }));
|
|
20
|
+
|
|
19
21
|
let nextId = 4;
|
|
20
22
|
|
|
21
23
|
function all() {
|
|
@@ -43,7 +45,13 @@ function query(whereClause) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function insert(note) {
|
|
46
|
-
const record = { id: nextId++, secret: false,
|
|
48
|
+
const record = { id: nextId++, secret: false, tags: [], archived: false, createdAt: new Date().toISOString() };
|
|
49
|
+
// Only override a default when the caller actually supplied the key — a
|
|
50
|
+
// spread of an object containing an explicit `undefined` would otherwise
|
|
51
|
+
// silently clobber the default (e.g. `tags: undefined`).
|
|
52
|
+
for (const [key, value] of Object.entries(note)) {
|
|
53
|
+
if (value !== undefined) record[key] = value;
|
|
54
|
+
}
|
|
47
55
|
notes.push(record);
|
|
48
56
|
return record;
|
|
49
57
|
}
|
|
@@ -55,12 +63,29 @@ function remove(id) {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
function reset() {
|
|
58
|
-
notes =
|
|
59
|
-
{ id: 1, owner: 'alice', title: 'Buy milk', body: 'Whole milk, 2 liters', secret: false },
|
|
60
|
-
{ id: 2, owner: 'alice', title: 'Launch plan', body: 'Ship the demo repo', secret: true },
|
|
61
|
-
{ id: 3, owner: 'bob', title: 'Gym', body: 'Leg day', secret: false },
|
|
62
|
-
];
|
|
66
|
+
notes = SEED.map((n) => ({ ...n }));
|
|
63
67
|
nextId = 4;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
// Returns a NEW array sorted by `id` ascending — does not mutate `arr`.
|
|
71
|
+
//
|
|
72
|
+
// >>> DEMO NOTE: bubble sort is an INTENTIONAL, isolated inefficiency (O(n^2))
|
|
73
|
+
// >>> kept here so the review/perf pipeline has a real hot path to optimize.
|
|
74
|
+
// >>> `notes` is already maintained in insertion (id) order, so callers that
|
|
75
|
+
// >>> only need "sorted by id" rarely need this at all — see the perf issue
|
|
76
|
+
// >>> that targets its one caller in routes/notes.js.
|
|
77
|
+
function bubbleSortById(arr) {
|
|
78
|
+
const copy = arr.slice();
|
|
79
|
+
for (let i = 0; i < copy.length; i++) {
|
|
80
|
+
for (let j = 0; j < copy.length - i - 1; j++) {
|
|
81
|
+
if (copy[j].id > copy[j + 1].id) {
|
|
82
|
+
const tmp = copy[j];
|
|
83
|
+
copy[j] = copy[j + 1];
|
|
84
|
+
copy[j + 1] = tmp;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return copy;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { all, findById, query, insert, remove, reset, bubbleSortById };
|
|
@@ -8,17 +8,80 @@ const db = require('../db');
|
|
|
8
8
|
const { requireToken } = require('../auth');
|
|
9
9
|
|
|
10
10
|
// GET /notes
|
|
11
|
-
// Lists notes. Supports
|
|
12
|
-
// into db.query().
|
|
11
|
+
// Lists notes. Supports:
|
|
12
|
+
// ?where= — raw expression passed straight into db.query() (see db.js)
|
|
13
|
+
// ?tag= — exact match against a note's `tags` array
|
|
14
|
+
// ?limit=, ?offset= — pagination
|
|
13
15
|
//
|
|
14
16
|
// >>> DEMO NOTE: forwarding the raw ?where= string into db.query() is the
|
|
15
17
|
// >>> injection surface (see db.js). Issue #2 asks to make this safe.
|
|
16
18
|
function listNotes(req, res, params) {
|
|
17
|
-
const where = params.query
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const { where, tag, limit, offset } = params.query;
|
|
20
|
+
|
|
21
|
+
let results = where ? db.query(where) : db.all();
|
|
22
|
+
|
|
23
|
+
if (tag) {
|
|
24
|
+
// >>> DEMO NOTE: `tags` is accepted but never validated as an array at
|
|
25
|
+
// >>> write time (see createNote below). A note created with a
|
|
26
|
+
// >>> malformed `tags` value makes this `.includes()` call throw instead
|
|
27
|
+
// >>> of being skipped safely. INTENTIONAL, isolated flaw.
|
|
28
|
+
results = results.filter((n) => n.tags.includes(tag));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (limit !== undefined || offset !== undefined) {
|
|
32
|
+
// >>> DEMO NOTE: `offset` is not clamped to a non-negative value, so a
|
|
33
|
+
// >>> negative offset falls through to Array.prototype.slice's
|
|
34
|
+
// >>> negative-index semantics (counts from the end) instead of being
|
|
35
|
+
// >>> rejected or normalized to 0. INTENTIONAL, isolated flaw.
|
|
36
|
+
const off = Number(offset) || 0;
|
|
37
|
+
const lim = Number(limit);
|
|
38
|
+
// Sorting by id here is redundant — `results` is already in id order —
|
|
39
|
+
// but the demo intentionally uses the O(n^2) helper anyway (see db.js).
|
|
40
|
+
const sorted = db.bubbleSortById(results);
|
|
41
|
+
results = sorted.slice(off, Number.isFinite(lim) ? off + lim : undefined);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { notes: results };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// GET /notes/count
|
|
48
|
+
// Returns { count } for notes matching optional ?owner= / ?secret= filters.
|
|
49
|
+
//
|
|
50
|
+
// >>> DEMO NOTE: builds its where-clause with naive string concatenation and
|
|
51
|
+
// >>> reuses the vulnerable db.query() eval path — the same injection class
|
|
52
|
+
// >>> issue #2 asks you to remove from listNotes, but reintroduced here in a
|
|
53
|
+
// >>> sibling endpoint issue #2's scope doesn't touch. INTENTIONAL, isolated
|
|
54
|
+
// >>> flaw kept for the benchmark corpus.
|
|
55
|
+
function countNotes(req, res, params) {
|
|
56
|
+
const { owner, secret } = params.query;
|
|
57
|
+
const clauses = [];
|
|
58
|
+
if (owner) clauses.push('owner === "' + owner + '"');
|
|
59
|
+
if (secret !== undefined) clauses.push('secret === ' + (secret === 'true'));
|
|
60
|
+
const whereClause = clauses.length ? clauses.join(' && ') : 'true';
|
|
61
|
+
return { count: db.query(whereClause).length };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// GET /notes/tags
|
|
65
|
+
// Returns the de-duplicated list of tags across all notes.
|
|
66
|
+
//
|
|
67
|
+
// >>> DEMO NOTE: de-dupes with a nested loop (checks every tag against every
|
|
68
|
+
// >>> unique tag seen so far) instead of a Set — O(n^2) on the total tag
|
|
69
|
+
// >>> count. INTENTIONAL, isolated inefficiency kept for the perf corpus.
|
|
70
|
+
function listTags(req, res, params) {
|
|
71
|
+
const unique = [];
|
|
72
|
+
for (const note of db.all()) {
|
|
73
|
+
for (const t of note.tags || []) {
|
|
74
|
+
let seen = false;
|
|
75
|
+
for (const u of unique) {
|
|
76
|
+
if (u === t) {
|
|
77
|
+
seen = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!seen) unique.push(t);
|
|
82
|
+
}
|
|
20
83
|
}
|
|
21
|
-
return {
|
|
84
|
+
return { tags: unique };
|
|
22
85
|
}
|
|
23
86
|
|
|
24
87
|
// GET /notes/:id
|
|
@@ -33,19 +96,53 @@ function getNote(req, res, params) {
|
|
|
33
96
|
}
|
|
34
97
|
|
|
35
98
|
// POST /notes (protected)
|
|
99
|
+
//
|
|
100
|
+
// >>> DEMO NOTE: accepts a `tags` field but never checks that it's an array
|
|
101
|
+
// >>> before storing it. A malformed value (e.g. a number) is stored as-is
|
|
102
|
+
// >>> and later crashes GET /notes?tag= (see listNotes). INTENTIONAL,
|
|
103
|
+
// >>> isolated flaw kept for the benchmark corpus.
|
|
36
104
|
function createNote(req, res, params) {
|
|
37
105
|
requireToken(req);
|
|
38
|
-
const { title, body, owner } = params.body || {};
|
|
106
|
+
const { title, body, owner, tags } = params.body || {};
|
|
39
107
|
if (!title) {
|
|
40
108
|
const err = new Error('title is required');
|
|
41
109
|
err.statusCode = 400;
|
|
42
110
|
throw err;
|
|
43
111
|
}
|
|
44
|
-
const
|
|
112
|
+
const payload = { title, body: body || '', owner: owner || 'anonymous' };
|
|
113
|
+
if (tags !== undefined) payload.tags = tags;
|
|
114
|
+
const note = db.insert(payload);
|
|
45
115
|
res.statusCode = 201;
|
|
46
116
|
return { note };
|
|
47
117
|
}
|
|
48
118
|
|
|
119
|
+
// PATCH /notes/:id (protected)
|
|
120
|
+
//
|
|
121
|
+
// >>> DEMO NOTE: applies `owner` and `secret` from the request body
|
|
122
|
+
// >>> unconditionally alongside `title`/`body`. Any caller holding the
|
|
123
|
+
// >>> shared demo token can reassign a note's ownership or flip its
|
|
124
|
+
// >>> `secret` flag to false, exposing it via the unauthenticated
|
|
125
|
+
// >>> GET /notes listing. This is a mass-assignment / over-permissive-write
|
|
126
|
+
// >>> flaw, distinct from issue #1's missing-auth-check pattern.
|
|
127
|
+
// >>> INTENTIONAL, isolated flaw kept for the benchmark corpus. Note that
|
|
128
|
+
// >>> `tags` is NOT among the fields this handler updates yet — that gap is
|
|
129
|
+
// >>> a separate, not-yet-built feature.
|
|
130
|
+
function updateNote(req, res, params) {
|
|
131
|
+
requireToken(req);
|
|
132
|
+
const note = db.findById(params.id);
|
|
133
|
+
if (!note) {
|
|
134
|
+
const err = new Error('Note not found');
|
|
135
|
+
err.statusCode = 404;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
const { title, body, owner, secret } = params.body || {};
|
|
139
|
+
if (title !== undefined) note.title = title;
|
|
140
|
+
if (body !== undefined) note.body = body;
|
|
141
|
+
if (owner !== undefined) note.owner = owner;
|
|
142
|
+
if (secret !== undefined) note.secret = secret;
|
|
143
|
+
return { note };
|
|
144
|
+
}
|
|
145
|
+
|
|
49
146
|
// DELETE /notes/:id
|
|
50
147
|
//
|
|
51
148
|
// >>> DEMO NOTE: this mutating route is MISSING a requireToken(req) call, so
|
|
@@ -61,4 +158,4 @@ function deleteNote(req, res, params) {
|
|
|
61
158
|
return { deleted: Number(params.id) };
|
|
62
159
|
}
|
|
63
160
|
|
|
64
|
-
module.exports = { listNotes, getNote, createNote, deleteNote };
|
|
161
|
+
module.exports = { listNotes, countNotes, listTags, getNote, createNote, updateNote, deleteNote };
|
|
@@ -14,7 +14,10 @@ const PORT = process.env.PORT || 3000;
|
|
|
14
14
|
const TABLE = [
|
|
15
15
|
['GET', /^\/notes$/, routes.listNotes],
|
|
16
16
|
['POST', /^\/notes$/, routes.createNote],
|
|
17
|
+
['GET', /^\/notes\/count$/, routes.countNotes],
|
|
18
|
+
['GET', /^\/notes\/tags$/, routes.listTags],
|
|
17
19
|
['GET', /^\/notes\/(?<id>\d+)$/, routes.getNote],
|
|
20
|
+
['PATCH', /^\/notes\/(?<id>\d+)$/, routes.updateNote],
|
|
18
21
|
['DELETE', /^\/notes\/(?<id>\d+)$/, routes.deleteNote],
|
|
19
22
|
];
|
|
20
23
|
|
|
@@ -58,7 +61,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
58
61
|
|
|
59
62
|
const [, pattern, handler] = match;
|
|
60
63
|
const groups = url.pathname.match(pattern).groups || {};
|
|
61
|
-
const body = req.method === 'POST' ? await readBody(req) : {};
|
|
64
|
+
const body = req.method === 'POST' || req.method === 'PATCH' ? await readBody(req) : {};
|
|
62
65
|
|
|
63
66
|
try {
|
|
64
67
|
const result = handler(req, res, { ...groups, query, body });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"!bin/tests/",
|
|
37
37
|
"commands/",
|
|
38
38
|
"docs/",
|
|
39
|
+
"!docs/site/.vitepress/",
|
|
39
40
|
"examples/",
|
|
41
|
+
"packages/protocol/src/",
|
|
40
42
|
"scripts/",
|
|
41
43
|
"templates/",
|
|
42
44
|
"LICENSE",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RapierCraft Studios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -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
|
+
}
|