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,189 @@
|
|
|
1
|
+
# Signed, Replayable Provenance Trail — Design Spec
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-04
|
|
4
|
+
**Status:** Design — ready to build
|
|
5
|
+
**Tracking:** #1318 (provenance layer), epic #1320 (five foundations of autonomy)
|
|
6
|
+
**Depends on:** #1256 (durable engine run-log — replayability requires a committed step log)
|
|
7
|
+
**Scope:** Integrity guarantee + replayability layer on top of the existing FORGE annotation protocol (#1291–1294). Every autonomous PR carries a signed attestation linking decisions → verification evidence → alternatives rejected.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Motivation
|
|
12
|
+
|
|
13
|
+
For an organization to allow autonomous merges to production, every change needs a complete, verifiable record of *why it was made, what was verified, and what alternatives were considered and rejected* — reproducibly.
|
|
14
|
+
|
|
15
|
+
Today provenance is scattered `FORGE:*` comments with no integrity guarantee:
|
|
16
|
+
|
|
17
|
+
| Gap | Current state | Problem |
|
|
18
|
+
|-----|--------------|---------|
|
|
19
|
+
| Integrity | Comments are plain Markdown, mutable | Any party can edit or delete them after the fact |
|
|
20
|
+
| Completeness | Each annotation covers one phase | No single artifact links decision → evidence → alternatives |
|
|
21
|
+
| Replayability | State is prose — humans can read it but tools cannot deterministically replay it | Resume re-parses prose heuristically |
|
|
22
|
+
| Attestation | No signed artifact exists | Human reviewers have no cryptographic basis to trust "the agent said it was safe" |
|
|
23
|
+
|
|
24
|
+
**Trust, not capability, is the blocker to auto-merge.** This spec adds integrity + replayability + a signed attestation on top of the existing FORGE protocol, without replacing it.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Goals / Non-goals
|
|
29
|
+
|
|
30
|
+
**Goals**
|
|
31
|
+
- Each autonomous PR carries a `FORGE:PROVENANCE` attestation that links: investigation decision → build decisions → verification evidence → rejected alternatives.
|
|
32
|
+
- A run is replayable from its provenance record + durable engine run-log (#1256).
|
|
33
|
+
- The provenance record validates against the FORGE protocol conformance suite (#1291).
|
|
34
|
+
- Integrity is achieved via a content hash over the structured annotation chain — no external PKI required for v1.
|
|
35
|
+
|
|
36
|
+
**Non-goals**
|
|
37
|
+
- Hardware-rooted signing (TPM, HSM) — out of scope for v1; the content hash gives tamper-evidence without it.
|
|
38
|
+
- Retroactive provenance for runs completed before this spec ships.
|
|
39
|
+
- Per-turn provenance (this spec is per-run / per-PR granularity, matching the durable engine's per-phase checkpoint).
|
|
40
|
+
- Provenance for interactive `/work-on` runs (headless/orchestrated first, interactive later).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 3. Locked Decisions
|
|
45
|
+
|
|
46
|
+
1. **Attestation format: structured HTML comment (`FORGE:PROVENANCE`) on the PR.** Consistent with the existing annotation protocol. Readable by humans in the PR thread and queryable by the same text-contains filters used today.
|
|
47
|
+
2. **Integrity mechanism: SHA-256 content hash over a canonical serialization of the annotation chain.** The hash is computed over the ordered sequence of `FORGE:*` annotation bodies (canonically serialized as JSON). Anyone with the run-log can recompute it.
|
|
48
|
+
3. **Replayability: the durable engine run-log is the replay substrate.** The `FORGE:PROVENANCE` record carries a pointer to the run-log location (issue comment ID sequence). A conformant replayer reads those comments in order and reconstructs the pipeline state.
|
|
49
|
+
4. **Chain of custody: each annotation carries a `seq` (sequence number) and `prev_hash` (hash of the prior annotation).** This forms a tamper-evident linked list. Adding, removing, or reordering annotations breaks the hash chain.
|
|
50
|
+
5. **The attestation is written by the durable engine, not by individual phase agents.** Agents write their phase annotations as today; the engine writes the final `FORGE:PROVENANCE` after the merge gate passes.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 4. Architecture
|
|
55
|
+
|
|
56
|
+
### 4.1 Annotation Chain Extension
|
|
57
|
+
|
|
58
|
+
Existing FORGE annotations gain two new fields (backward-compatible — consumers that do not understand them ignore them):
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
<!-- FORGE:INVESTIGATOR
|
|
62
|
+
seq: 1
|
|
63
|
+
prev_hash: "genesis"
|
|
64
|
+
...existing fields...
|
|
65
|
+
body_hash: "sha256:a1b2c3..."
|
|
66
|
+
-->
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| New field | Type | Description |
|
|
70
|
+
|-----------|------|-------------|
|
|
71
|
+
| `seq` | integer | Position in the annotation chain for this run (1-indexed) |
|
|
72
|
+
| `prev_hash` | string | SHA-256 of the prior annotation's canonical body, or `"genesis"` for the first |
|
|
73
|
+
| `body_hash` | string | SHA-256 of this annotation's canonical body (self-hash, written last) |
|
|
74
|
+
|
|
75
|
+
**Canonical body**: the annotation content between the `<!-- FORGE:TAG` opening and the `-->` closing, with leading/trailing whitespace stripped and line endings normalized to `\n`, serialized as a UTF-8 JSON string.
|
|
76
|
+
|
|
77
|
+
### 4.2 FORGE:PROVENANCE Annotation
|
|
78
|
+
|
|
79
|
+
Written to the PR by the durable engine after the merge gate passes, before the merge commit:
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<!-- FORGE:PROVENANCE
|
|
83
|
+
run_id: "run-abc123"
|
|
84
|
+
issue: 1317
|
|
85
|
+
pr: 1460
|
|
86
|
+
pipeline_version: "1.0.21"
|
|
87
|
+
agent_model: "claude-sonnet-4-6"
|
|
88
|
+
|
|
89
|
+
chain:
|
|
90
|
+
- seq: 1 tag: INVESTIGATOR comment_id: 2345678901 body_hash: "sha256:a1b2..."
|
|
91
|
+
- seq: 2 tag: ARCHITECT comment_id: 2345678910 body_hash: "sha256:b2c3..."
|
|
92
|
+
- seq: 3 tag: BUILDER comment_id: 2345678920 body_hash: "sha256:c3d4..."
|
|
93
|
+
- seq: 4 tag: REVIEWER comment_id: 2345678930 body_hash: "sha256:d4e5..."
|
|
94
|
+
- seq: 5 tag: QUALITY_GATE comment_id: 2345678940 body_hash: "sha256:e5f6..."
|
|
95
|
+
- seq: 6 tag: ECONOMICS comment_id: 2345678950 body_hash: "sha256:f6a7..."
|
|
96
|
+
|
|
97
|
+
chain_hash: "sha256:0123456789abcdef..."
|
|
98
|
+
verification_passed: true
|
|
99
|
+
alternatives_considered:
|
|
100
|
+
- "Inline fix without issue — rejected: no traceability"
|
|
101
|
+
- "Defer to manual review — rejected: low-risk score 0.82"
|
|
102
|
+
merge_authorized_by: "economic-self-governance"
|
|
103
|
+
timestamp: 2026-07-04T18:00:00Z
|
|
104
|
+
forge_protocol_version: "1.0"
|
|
105
|
+
-->
|
|
106
|
+
<!-- FORGE:PROVENANCE:COMPLETE -->
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`chain_hash`**: SHA-256 of the concatenated `body_hash` values in `seq` order, joined with `\n`. Recomputable from the listed comment IDs alone.
|
|
110
|
+
|
|
111
|
+
### 4.3 Replayability Contract
|
|
112
|
+
|
|
113
|
+
A conformant replayer MUST be able to reconstruct the pipeline state for a given `run_id` by:
|
|
114
|
+
|
|
115
|
+
1. Reading the `FORGE:PROVENANCE` annotation from the PR to get the `chain` (comment ID list).
|
|
116
|
+
2. Fetching each comment in `seq` order from the GitHub API.
|
|
117
|
+
3. Parsing each `FORGE:*` annotation body.
|
|
118
|
+
4. Recomputing the `chain_hash` and asserting it matches the recorded value.
|
|
119
|
+
5. Replaying the durable engine run-log (from `#1256`) for per-phase step details.
|
|
120
|
+
|
|
121
|
+
Step 4 is the integrity check. A mismatch means the annotation chain was tampered with after the run.
|
|
122
|
+
|
|
123
|
+
### 4.4 Conformance Suite Integration (#1291)
|
|
124
|
+
|
|
125
|
+
The FORGE protocol conformance suite gains a new test category: **Provenance Conformance**.
|
|
126
|
+
|
|
127
|
+
| Test | Pass condition |
|
|
128
|
+
|------|---------------|
|
|
129
|
+
| `P-01` | PR has exactly one `FORGE:PROVENANCE` annotation followed by `FORGE:PROVENANCE:COMPLETE` |
|
|
130
|
+
| `P-02` | All `seq` values in the chain are contiguous starting from 1 |
|
|
131
|
+
| `P-03` | Each `prev_hash` matches the `body_hash` of the prior annotation (or `"genesis"` for seq=1) |
|
|
132
|
+
| `P-04` | `chain_hash` recomputes correctly from the listed `body_hash` values |
|
|
133
|
+
| `P-05` | All annotations referenced in the chain are fetchable and parse as valid FORGE annotations |
|
|
134
|
+
| `P-06` | `timestamp` is an ISO 8601 UTC datetime |
|
|
135
|
+
| `P-07` | `forge_protocol_version` is present and a known value |
|
|
136
|
+
|
|
137
|
+
### 4.5 Integration Points
|
|
138
|
+
|
|
139
|
+
**Durable engine (`#1256`)**: After merge gate passes, the engine invokes `write_provenance()`:
|
|
140
|
+
- Collects all annotation comment IDs from the run-log.
|
|
141
|
+
- Fetches each annotation body and computes `body_hash` values.
|
|
142
|
+
- Computes `chain_hash`.
|
|
143
|
+
- Posts `FORGE:PROVENANCE` to the PR.
|
|
144
|
+
|
|
145
|
+
**Quality gate (`/quality-gate`)**: Gains a new check: `PROVENANCE` — verifies `P-01` through `P-07`. Fails the gate if any check fails on an autonomous run. Human-initiated runs emit a warning (not a hard failure) if provenance is absent.
|
|
146
|
+
|
|
147
|
+
**`/replay` command**: Uses the `chain` from `FORGE:PROVENANCE` as the replay index instead of scanning all comments for `FORGE:*` annotations. This is faster and guaranteed-ordered.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 5. Implementation Plan
|
|
152
|
+
|
|
153
|
+
### Increment 1: Chain fields on existing annotations (non-breaking)
|
|
154
|
+
- Modify durable engine to write `seq`, `prev_hash`, and `body_hash` on each annotation it posts.
|
|
155
|
+
- No consumer changes required — fields are additive.
|
|
156
|
+
- Validate that existing annotations without these fields are still parseable.
|
|
157
|
+
|
|
158
|
+
### Increment 2: FORGE:PROVENANCE annotation
|
|
159
|
+
- Engine writes `FORGE:PROVENANCE` to the PR after merge gate passes.
|
|
160
|
+
- Includes `chain`, `chain_hash`, `verification_passed`, `timestamp`.
|
|
161
|
+
- `/replay` updated to use the chain index when present.
|
|
162
|
+
|
|
163
|
+
### Increment 3: Conformance tests + quality gate check
|
|
164
|
+
- Add `P-01` through `P-07` tests to the conformance suite (#1291).
|
|
165
|
+
- Add `PROVENANCE` check to `/quality-gate` (warning on human runs, hard failure on autonomous runs).
|
|
166
|
+
|
|
167
|
+
### Increment 4: Replayability validation
|
|
168
|
+
- Add a `verify_provenance(run_id)` script to `scripts/`.
|
|
169
|
+
- Script fetches the PR, reads `FORGE:PROVENANCE`, fetches all chained comments, recomputes `chain_hash`, and reports pass/fail.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 6. Open Questions
|
|
174
|
+
|
|
175
|
+
- **Comment mutability**: GitHub allows editing comments. The hash chain detects tampering but does not prevent it. For v2, consider posting annotations to an immutable artifact (e.g., a GitHub Release asset or a signed commit note) as an optional hardened mode.
|
|
176
|
+
- **`body_hash` for legacy annotations**: If an annotation was written before this spec (no `body_hash` field), the chain is broken. Acceptable for v1: the `FORGE:PROVENANCE` chain only covers annotations written by the durable engine (which will be updated atomically). Legacy runs have no provenance claim.
|
|
177
|
+
- **`alternatives_considered` source**: In v1 this is written by the engine based on the economics decision (#1317) and the quality-gate outcome. A richer alternatives record (e.g., architect alternatives from `FORGE:ARCHITECT`) can be pulled in a later increment.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 7. References
|
|
182
|
+
|
|
183
|
+
- #1318 — this issue
|
|
184
|
+
- #1291–1294 — FORGE protocol conformance suite (seed schema)
|
|
185
|
+
- #1256 — durable engine (run-log substrate; required for replayability)
|
|
186
|
+
- #1315 — outcome-based acceptance gate (supplies "what was verified" evidence)
|
|
187
|
+
- #1317 — economic self-governance (supplies `merge_authorized_by` and `alternatives_considered`)
|
|
188
|
+
- #1320 — five foundations epic (provenance layer)
|
|
189
|
+
- `docs/spec/forge-protocol-v1.md` — FORGE annotation protocol v1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ForgeDock Demo — A Risk-Free First Pipeline Run
|
|
2
2
|
|
|
3
|
-
A tiny **Notes API** (~
|
|
3
|
+
A tiny **Notes API** (~400 lines, zero dependencies) with twenty pre-written
|
|
4
4
|
issues. It exists so you can watch ForgeDock investigate, build, review, and
|
|
5
5
|
merge a change **without touching your real codebase**.
|
|
6
6
|
|
|
@@ -14,9 +14,10 @@ Trying ForgeDock on your own project means trusting an AI pipeline with real
|
|
|
14
14
|
code on your very first run. This demo removes that risk: it's a self-contained
|
|
15
15
|
sandbox where every issue is small, fast, and safe to break.
|
|
16
16
|
|
|
17
|
-
The API has a
|
|
18
|
-
check,
|
|
19
|
-
findings to surface — that's the part
|
|
17
|
+
The API has a handful of **intentional, clearly-labelled flaws** (a missing auth
|
|
18
|
+
check, two injection-vulnerable query paths, a mass-assignment gap) so that
|
|
19
|
+
ForgeDock's `/review-pr` agents have real findings to surface — that's the part
|
|
20
|
+
that makes the review feel impressive.
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -27,11 +28,11 @@ forgedock-demo/
|
|
|
27
28
|
├── src/
|
|
28
29
|
│ ├── server.js # built-in http server + router (no Express)
|
|
29
30
|
│ ├── router.js # (added by Issue #3)
|
|
30
|
-
│ ├── routes/notes.js # CRUD handlers — has
|
|
31
|
-
│ ├── db.js # in-memory store
|
|
31
|
+
│ ├── routes/notes.js # CRUD + count/tags/patch handlers — has intentional flaws
|
|
32
|
+
│ ├── db.js # in-memory store with tags, createdAt, archived fields
|
|
32
33
|
│ └── auth.js # bearer-token check
|
|
33
34
|
├── scripts/smoke.js # dependency-free smoke test (npm run smoke)
|
|
34
|
-
├── issues/ #
|
|
35
|
+
├── issues/ # 20 graded issue specs
|
|
35
36
|
├── forge.yaml # minimal working ForgeDock config
|
|
36
37
|
├── labels.json # workflow + priority labels
|
|
37
38
|
└── bootstrap.sh # one command to stand up the live repo
|
|
@@ -52,20 +53,37 @@ npm run smoke # runs the smoke test
|
|
|
52
53
|
|
|
53
54
|
---
|
|
54
55
|
|
|
55
|
-
## The
|
|
56
|
-
|
|
57
|
-
Each issue targets a different pipeline strength and
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
56
|
+
## The twenty demo issues
|
|
57
|
+
|
|
58
|
+
Each issue targets a different pipeline strength and difficulty tier.
|
|
59
|
+
Issues 1–5 are the original quick-start set; issues 6–20 expand the corpus
|
|
60
|
+
for statistically meaningful one-shot benchmark runs.
|
|
61
|
+
|
|
62
|
+
| # | Difficulty | Type | Title |
|
|
63
|
+
|---|-----------|------|-------|
|
|
64
|
+
| 1 | Easy | Bug / security | DELETE is missing an auth check |
|
|
65
|
+
| 2 | Medium | Feature / security | Safe filtering for `GET /notes` |
|
|
66
|
+
| 3 | Easy | Refactor | Extract the router module |
|
|
67
|
+
| 4 | Medium | Performance | O(1) `findById` |
|
|
68
|
+
| 5 | Easy | Docs | Add an API reference |
|
|
69
|
+
| 6 | Medium | Bug | POST stores malformed tags without validation |
|
|
70
|
+
| 7 | Medium | Bug | Negative `?offset=` returns wrong notes instead of 400 |
|
|
71
|
+
| 8 | Hard | Bug / security | PATCH allows mass-assignment of owner and secret |
|
|
72
|
+
| 9 | Hard | Bug / security | GET /notes/count is vulnerable to expression injection |
|
|
73
|
+
| 10 | Easy | Feature | Add `?sort=` parameter to GET /notes |
|
|
74
|
+
| 11 | Medium | Feature | Allow PATCH to update tags |
|
|
75
|
+
| 12 | Hard | Feature | POST /notes/bulk for all-or-nothing batch creation |
|
|
76
|
+
| 13 | Hard | Feature | Archive/soft-delete with `?archived=` list filter |
|
|
77
|
+
| 14 | Easy | Refactor | Extract title validation into validate.js |
|
|
78
|
+
| 15 | Medium | Refactor | Centralize auth enforcement with a protected route flag |
|
|
79
|
+
| 16 | Hard | Refactor | Extract generic store machinery into store.js |
|
|
80
|
+
| 17 | Easy | Performance | Replace O(n²) tag de-dup in listTags with a Set |
|
|
81
|
+
| 18 | Medium | Performance | Remove unnecessary O(n²) sort before pagination slice |
|
|
82
|
+
| 19 | Easy | Docs | Complete API reference covering all endpoints |
|
|
83
|
+
| 20 | Easy | Docs | Document the intentional-flaw and graded-issue convention |
|
|
66
84
|
|
|
67
85
|
Full specs live in [`issues/`](./issues). When you run `bootstrap.sh`, these
|
|
68
|
-
become real GitHub issues #1–#
|
|
86
|
+
become real GitHub issues #1–#20 in your demo repo, in this order.
|
|
69
87
|
|
|
70
88
|
---
|
|
71
89
|
|
|
@@ -87,7 +105,7 @@ credentials is creating the actual repo. One command does it:
|
|
|
87
105
|
2. Push this codebase to `main`.
|
|
88
106
|
3. Create the workflow + priority labels (via `npx forgedock labels setup`,
|
|
89
107
|
falling back to `labels.json`).
|
|
90
|
-
4. File
|
|
108
|
+
4. File all twenty issues from [`issues/`](./issues).
|
|
91
109
|
|
|
92
110
|
Then:
|
|
93
111
|
|
|
@@ -106,8 +124,9 @@ gate, open a PR, review it, and merge — all on a repo you can safely throw awa
|
|
|
106
124
|
|
|
107
125
|
## After your first run
|
|
108
126
|
|
|
109
|
-
Try `/work-on 2` through `/work-on
|
|
110
|
-
`/orchestrate` to let ForgeDock work several issues in
|
|
127
|
+
Try `/work-on 2` through `/work-on 20` to see the full range of pipeline phases
|
|
128
|
+
and issue types, or run `/orchestrate` to let ForgeDock work several issues in
|
|
129
|
+
parallel.
|
|
111
130
|
|
|
112
131
|
When you're comfortable, point ForgeDock at your real project — you've already
|
|
113
132
|
seen exactly what it does.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): POST /notes stores malformed tags without validation"
|
|
3
|
+
labels: ["bug", "priority:P2"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`POST /notes` accepts a `tags` field from the request body but stores it without
|
|
10
|
+
checking that it is an array. When a caller passes `"tags": 42` (or any non-array
|
|
11
|
+
value), the malformed value is saved to the in-memory store. A subsequent
|
|
12
|
+
`GET /notes?tag=work` then throws an uncaught `TypeError: n.tags.includes is not
|
|
13
|
+
a function` from the tag-filter in `listNotes()`, returning a 500 response for
|
|
14
|
+
all future tag-filtered requests — until the server is restarted.
|
|
15
|
+
|
|
16
|
+
## Steps to Reproduce
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Create a note with malformed tags
|
|
20
|
+
curl -s -X POST http://localhost:3000/notes \
|
|
21
|
+
-H "Authorization: Bearer demo-token" \
|
|
22
|
+
-H "Content-Type: application/json" \
|
|
23
|
+
-d '{"title":"bad","tags":99}' | jq .
|
|
24
|
+
|
|
25
|
+
# 2. Any tag-filtered list now 500s
|
|
26
|
+
curl -s "http://localhost:3000/notes?tag=work" | jq .
|
|
27
|
+
# => {"error":"n.tags.includes is not a function"}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Affected Files
|
|
31
|
+
|
|
32
|
+
1. `src/routes/notes.js` — `createNote()` must validate that `tags`, when
|
|
33
|
+
supplied, is an array. Return 400 if it is not.
|
|
34
|
+
|
|
35
|
+
## Acceptance Criteria
|
|
36
|
+
|
|
37
|
+
- [ ] `POST /notes` with `"tags": 42` returns `400 Bad Request`.
|
|
38
|
+
- [ ] `POST /notes` with `"tags": ["work"]` still succeeds (201).
|
|
39
|
+
- [ ] `GET /notes?tag=work` works correctly after a valid note with tags is created.
|
|
40
|
+
- [ ] `npm run smoke` still passes.
|
|
41
|
+
|
|
42
|
+
> This issue exercises the **validation gap** pattern — the pipeline should
|
|
43
|
+
> add a type check at the write boundary so the read path stays safe.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): negative ?offset= returns wrong notes instead of 400"
|
|
3
|
+
labels: ["bug", "priority:P2"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes?offset=-1` silently applies JavaScript's negative-index slice
|
|
10
|
+
semantics (`Array.prototype.slice(-1)` returns the last element) instead of
|
|
11
|
+
returning a client error. A caller expecting an empty or 400 response receives
|
|
12
|
+
the final note in the list, which is confusing and incorrect.
|
|
13
|
+
|
|
14
|
+
## Steps to Reproduce
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# List all notes first
|
|
18
|
+
curl -s http://localhost:3000/notes | jq '.notes | length'
|
|
19
|
+
# => 3
|
|
20
|
+
|
|
21
|
+
# Negative offset returns the last note — unexpected
|
|
22
|
+
curl -s "http://localhost:3000/notes?offset=-1" | jq '.notes'
|
|
23
|
+
# => [{...note 3...}]
|
|
24
|
+
|
|
25
|
+
# Expected: 400 Bad Request or normalized to 0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Affected Files
|
|
29
|
+
|
|
30
|
+
1. `src/routes/notes.js` — `listNotes()` must clamp `offset` to a non-negative
|
|
31
|
+
integer (reject with 400, or normalize to 0) before passing it to
|
|
32
|
+
`Array.prototype.slice`.
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] `GET /notes?offset=-1` returns `400 Bad Request` with a clear error message.
|
|
37
|
+
- [ ] `GET /notes?offset=0` still returns all notes.
|
|
38
|
+
- [ ] `GET /notes?offset=1&limit=2` returns notes starting from index 1.
|
|
39
|
+
- [ ] `npm run smoke` still passes.
|
|
40
|
+
|
|
41
|
+
> This issue demonstrates that **numeric input validation** is a separate concern
|
|
42
|
+
> from type validation — the value is a number but falls outside the accepted range.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): PATCH /notes/:id allows mass-assignment of owner and secret"
|
|
3
|
+
labels: ["bug", "security", "priority:P1"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`PATCH /notes/:id` lets any authenticated caller overwrite the `owner` and
|
|
10
|
+
`secret` fields alongside `title` and `body`. This is a mass-assignment
|
|
11
|
+
vulnerability: a caller can flip `"secret": true` notes to `false`, making them
|
|
12
|
+
visible in unauthenticated `GET /notes` responses, or reassign `owner` to
|
|
13
|
+
impersonate another user.
|
|
14
|
+
|
|
15
|
+
Unlike issue #1 (which has no auth check at all), this endpoint does require a
|
|
16
|
+
token — the flaw is that it accepts a too-wide field set.
|
|
17
|
+
|
|
18
|
+
## Steps to Reproduce
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Flip a secret note to non-secret without knowing its content
|
|
22
|
+
curl -s -X PATCH http://localhost:3000/notes/2 \
|
|
23
|
+
-H "Authorization: Bearer demo-token" \
|
|
24
|
+
-H "Content-Type: application/json" \
|
|
25
|
+
-d '{"secret":false}' | jq .
|
|
26
|
+
# => note is now visible to unauthenticated callers
|
|
27
|
+
|
|
28
|
+
# Confirm it is now publicly listed
|
|
29
|
+
curl -s http://localhost:3000/notes | jq '[.notes[] | select(.id==2)]'
|
|
30
|
+
# => [{...secret note now visible...}]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Affected Files
|
|
34
|
+
|
|
35
|
+
1. `src/routes/notes.js` — `updateNote()` must restrict the allowed fields to
|
|
36
|
+
`title` and `body` only. Attempts to set `owner` or `secret` should return
|
|
37
|
+
`400 Bad Request` with a message listing the disallowed fields.
|
|
38
|
+
|
|
39
|
+
## Acceptance Criteria
|
|
40
|
+
|
|
41
|
+
- [ ] `PATCH /notes/:id` with `{"secret": false}` returns `400 Bad Request`.
|
|
42
|
+
- [ ] `PATCH /notes/:id` with `{"owner": "hacker"}` returns `400 Bad Request`.
|
|
43
|
+
- [ ] `PATCH /notes/:id` with `{"title": "new"}` still succeeds and updates the title.
|
|
44
|
+
- [ ] `PATCH /notes/:id` with both `{"title": "new", "secret": false}` returns `400`.
|
|
45
|
+
- [ ] `npm run smoke` still passes.
|
|
46
|
+
|
|
47
|
+
> This issue covers the **mass-assignment / over-permissive write** pattern — a
|
|
48
|
+
> common class of security bugs where an update endpoint accepts more fields than
|
|
49
|
+
> it should.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "fix(notes): GET /notes/count is vulnerable to expression injection"
|
|
3
|
+
labels: ["bug", "security", "priority:P1"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes/count` builds its `db.query()` where-clause by concatenating the raw
|
|
10
|
+
`?owner=` query parameter into a string: `'owner === "' + owner + '"'`. This is
|
|
11
|
+
the same injection class as the `?where=` flaw in issue #2, but in a new
|
|
12
|
+
endpoint that issue #2's fix does not cover.
|
|
13
|
+
|
|
14
|
+
A caller can inject arbitrary JavaScript: `?owner=" || true; //` would evaluate
|
|
15
|
+
to `owner === "" || true; //` — matching every note and leaking the real count
|
|
16
|
+
regardless of owner.
|
|
17
|
+
|
|
18
|
+
## Steps to Reproduce
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Normal count
|
|
22
|
+
curl -s "http://localhost:3000/notes/count?owner=alice" | jq .
|
|
23
|
+
# => {"count":2}
|
|
24
|
+
|
|
25
|
+
# Injection — count all notes regardless of owner
|
|
26
|
+
curl -s 'http://localhost:3000/notes/count?owner=" || true; //' | jq .
|
|
27
|
+
# => {"count":3} (all notes leaked)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Affected Files
|
|
31
|
+
|
|
32
|
+
1. `src/routes/notes.js` — `countNotes()` must not build a `db.query()` clause
|
|
33
|
+
via string concatenation. Replace the `db.query()` call with a direct
|
|
34
|
+
`db.all().filter()` using strict equality checks on typed values.
|
|
35
|
+
|
|
36
|
+
## Acceptance Criteria
|
|
37
|
+
|
|
38
|
+
- [ ] `GET /notes/count?owner=alice` returns `{"count": 2}`.
|
|
39
|
+
- [ ] `GET /notes/count?owner=" || true; //` returns `{"count": 0}` (no match,
|
|
40
|
+
not a leak).
|
|
41
|
+
- [ ] `GET /notes/count?secret=true` returns the correct count of secret notes.
|
|
42
|
+
- [ ] `GET /notes/count` (no filters) returns the total note count.
|
|
43
|
+
- [ ] `npm run smoke` still passes.
|
|
44
|
+
|
|
45
|
+
> This issue reinforces the pattern from issue #2 — injection through dynamic
|
|
46
|
+
> `db.query()` clause building — but in a different endpoint, demonstrating that
|
|
47
|
+
> fixing the first occurrence doesn't automatically close the class.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): add ?sort= parameter to GET /notes"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "easy"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`GET /notes` always returns notes in insertion order. There is no way for a
|
|
10
|
+
caller to request notes sorted by `title` or `createdAt` without fetching all
|
|
11
|
+
notes and sorting client-side.
|
|
12
|
+
|
|
13
|
+
## Proposed Solution
|
|
14
|
+
|
|
15
|
+
Add a `?sort=` query parameter to `GET /notes` that accepts:
|
|
16
|
+
|
|
17
|
+
- `sort=id` — sort by numeric id ascending (current default behavior)
|
|
18
|
+
- `sort=title` — sort alphabetically by `title`
|
|
19
|
+
- `sort=createdAt` — sort by `createdAt` string (ISO 8601, lexicographic sort
|
|
20
|
+
is equivalent to chronological for the demo's fixed-format timestamps)
|
|
21
|
+
|
|
22
|
+
An unknown `sort` value should return `400 Bad Request`.
|
|
23
|
+
|
|
24
|
+
## Affected Files
|
|
25
|
+
|
|
26
|
+
1. `src/routes/notes.js` — extend `listNotes()` to read a `sort` query param
|
|
27
|
+
and apply the appropriate comparison before returning the results array.
|
|
28
|
+
|
|
29
|
+
## Acceptance Criteria
|
|
30
|
+
|
|
31
|
+
- [ ] `GET /notes?sort=title` returns notes sorted alphabetically by title.
|
|
32
|
+
- [ ] `GET /notes?sort=createdAt` returns notes sorted oldest-first.
|
|
33
|
+
- [ ] `GET /notes?sort=id` returns notes in id order (same as current default).
|
|
34
|
+
- [ ] `GET /notes?sort=unknown` returns `400 Bad Request`.
|
|
35
|
+
- [ ] `GET /notes` (no sort param) still returns notes in insertion order.
|
|
36
|
+
- [ ] `npm run smoke` still passes.
|
|
37
|
+
|
|
38
|
+
> A focused, additive feature — the pipeline should confirm no existing
|
|
39
|
+
> behavior changes and that the 400 guard is present.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): allow PATCH /notes/:id to update tags"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "medium"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`PATCH /notes/:id` currently only updates `title` and `body` (after the fix from
|
|
10
|
+
issue #8). There is no way to add or replace tags on an existing note without
|
|
11
|
+
deleting and recreating it.
|
|
12
|
+
|
|
13
|
+
## Proposed Solution
|
|
14
|
+
|
|
15
|
+
Extend `PATCH /notes/:id` to accept an optional `tags` field. The value must be
|
|
16
|
+
an array of strings (validate at write time, same fix pattern as issue #6).
|
|
17
|
+
When supplied, it replaces the note's existing `tags` array entirely — a full
|
|
18
|
+
replacement (not a merge/append), keeping the semantics simple.
|
|
19
|
+
|
|
20
|
+
## Affected Files
|
|
21
|
+
|
|
22
|
+
1. `src/routes/notes.js` — `updateNote()` should accept `tags` in addition to
|
|
23
|
+
`title` and `body`. Validate that `tags` is an array when present; return
|
|
24
|
+
`400` if it is not.
|
|
25
|
+
|
|
26
|
+
## Acceptance Criteria
|
|
27
|
+
|
|
28
|
+
- [ ] `PATCH /notes/:id` with `{"tags": ["work", "urgent"]}` replaces the note's
|
|
29
|
+
tags and returns the updated note.
|
|
30
|
+
- [ ] `PATCH /notes/:id` with `{"tags": 99}` returns `400 Bad Request`.
|
|
31
|
+
- [ ] Omitting `tags` from the patch body leaves existing tags unchanged.
|
|
32
|
+
- [ ] `GET /notes?tag=urgent` returns the patched note after its tags are updated.
|
|
33
|
+
- [ ] `npm run smoke` still passes.
|
|
34
|
+
|
|
35
|
+
> This feature has a deliberate dependency on issue #8 being fixed first — tags
|
|
36
|
+
> must not be writable via mass-assignment before they are intentionally writable
|
|
37
|
+
> via a validated field. Run issue #8 before this one.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): add POST /notes/bulk for all-or-nothing batch creation"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Creating multiple notes requires a separate `POST /notes` call per note.
|
|
10
|
+
For loading test fixtures or seeding data, callers want to create several notes
|
|
11
|
+
in one request, with the guarantee that either all are created or none are
|
|
12
|
+
(all-or-nothing semantics).
|
|
13
|
+
|
|
14
|
+
## Proposed Solution
|
|
15
|
+
|
|
16
|
+
Add `POST /notes/bulk` (protected) that accepts `{ "notes": [...] }` where each
|
|
17
|
+
item has the same shape as the single-note `POST /notes` body. The endpoint
|
|
18
|
+
must:
|
|
19
|
+
|
|
20
|
+
1. Validate that the top-level `notes` field is a non-empty array.
|
|
21
|
+
2. Validate that every item has a non-empty `title`.
|
|
22
|
+
3. Only insert any notes if all items are valid (all-or-nothing).
|
|
23
|
+
4. Return `{ "notes": [...created notes] }` with status 201 on success.
|
|
24
|
+
|
|
25
|
+
## Affected Files
|
|
26
|
+
|
|
27
|
+
1. `src/routes/notes.js` — add a new `bulkCreateNotes()` handler.
|
|
28
|
+
2. `src/server.js` — register `POST /notes/bulk` in the route table **before**
|
|
29
|
+
the existing `POST /notes` entry so the literal path match takes priority.
|
|
30
|
+
|
|
31
|
+
## Acceptance Criteria
|
|
32
|
+
|
|
33
|
+
- [ ] `POST /notes/bulk` with a valid array creates all notes and returns 201.
|
|
34
|
+
- [ ] If any note is missing `title`, returns `400` and no notes are created.
|
|
35
|
+
- [ ] `POST /notes/bulk` without a token returns `401`.
|
|
36
|
+
- [ ] `POST /notes/bulk` with `{"notes": []}` returns `400`.
|
|
37
|
+
- [ ] `GET /notes` after a successful bulk create shows the new notes.
|
|
38
|
+
- [ ] `npm run smoke` still passes.
|
|
39
|
+
|
|
40
|
+
> This feature requires careful thought about route ordering (the `/bulk`
|
|
41
|
+
> literal path must not be shadowed by `POST /notes$`) and about transaction
|
|
42
|
+
> semantics in the in-memory store.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "feat(notes): implement archive/soft-delete with ?archived= list filter"
|
|
3
|
+
labels: ["feature", "priority:P3"]
|
|
4
|
+
difficulty: "hard"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`DELETE /notes/:id` permanently removes a note. There is no way to soft-delete
|
|
10
|
+
(archive) a note and later restore it or filter archived notes out of the default
|
|
11
|
+
listing. The `archived` field already exists in the data model but is not
|
|
12
|
+
writable or filterable.
|
|
13
|
+
|
|
14
|
+
## Proposed Solution
|
|
15
|
+
|
|
16
|
+
1. Add `POST /notes/:id/archive` (protected) — sets `archived: true` on the
|
|
17
|
+
note. Returns the updated note.
|
|
18
|
+
2. Add `POST /notes/:id/restore` (protected) — sets `archived: false`.
|
|
19
|
+
3. Extend `GET /notes` to support `?archived=true` (list only archived notes),
|
|
20
|
+
`?archived=false` (list only non-archived notes — new default behavior), and
|
|
21
|
+
no `archived` param (list all, preserving today's behavior).
|
|
22
|
+
|
|
23
|
+
By default (no `?archived=` param) `GET /notes` should still return all notes,
|
|
24
|
+
including archived ones, to avoid a breaking change for the existing smoke test.
|
|
25
|
+
|
|
26
|
+
## Affected Files
|
|
27
|
+
|
|
28
|
+
1. `src/routes/notes.js` — add `archiveNote()` and `restoreNote()` handlers;
|
|
29
|
+
extend `listNotes()` to apply an `archived` filter when the param is present.
|
|
30
|
+
2. `src/server.js` — register the two new routes:
|
|
31
|
+
- `POST /notes/:id/archive`
|
|
32
|
+
- `POST /notes/:id/restore`
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] `POST /notes/1/archive` (with token) returns `{"note": {..., "archived": true}}`.
|
|
37
|
+
- [ ] `POST /notes/1/restore` (with token) returns `{"note": {..., "archived": false}}`.
|
|
38
|
+
- [ ] `GET /notes?archived=false` omits archived notes.
|
|
39
|
+
- [ ] `GET /notes?archived=true` returns only archived notes.
|
|
40
|
+
- [ ] `GET /notes` (no param) returns all notes regardless of archived status.
|
|
41
|
+
- [ ] Both new routes return `401` without a token and `404` for unknown ids.
|
|
42
|
+
- [ ] `npm run smoke` still passes.
|
|
43
|
+
|
|
44
|
+
> A multi-route feature that touches both handler and server wiring. Good for
|
|
45
|
+
> testing whether the pipeline correctly sequences route registration and
|
|
46
|
+
> confirms no regression on the existing list/create/delete flows.
|