@warpgogol/forge 2.21.5 → 2.21.7
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/AGENTS.md +27 -1
- package/README.md +93 -0
- package/README.uk.md +93 -0
- package/os/adr/adr-0000-template.md +8 -0
- package/os/adr/handlers/validate.test.ts +203 -0
- package/os/adr/handlers/validate.ts +54 -1
- package/os/adr/types.ts +7 -0
- package/os/compass/handlers/compass-inventory-handler.ts +11 -1
- package/os/compass/handlers/compass-inventory.ts +10 -0
- package/os/core/handlers/validate.ts +56 -5
- package/os/naming/naming-convention.ts +9 -0
- package/os/plugin/plugin.module.ts +1 -1
- package/os/rfc/acceptance.ts +133 -4
- package/os/rfc/handlers/implement-stamp.ts +14 -1
- package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
- package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
- package/os/rfc/handlers/validate-rules.ts +450 -9
- package/os/rfc/handlers/validate.ts +20 -2
- package/os/rfc/rfc-0000-template.md +24 -8
- package/os/rfc/rfc.module.ts +28 -0
- package/os/rfc/types.ts +72 -6
- package/os/rfc/verification-evidence.ts +5 -4
- package/os/rfc/verification-refresh.test.ts +320 -0
- package/os/rfc/verification-refresh.ts +216 -0
- package/os/session/handlers/save.ts +10 -0
- package/os/spec/spec-validate.test.ts +59 -0
- package/os/spec/spec-validate.ts +6 -4
- package/package.json +2 -1
- package/skills/fo/fo-handoff/SKILL.md +15 -6
- package/skills/fo/fo-idea-audit/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
- package/skills/fo/fo-idea-implement/SKILL.md +3 -2
- package/src/compass/contract-registry.ts +25 -6
- package/src/index.ts +1 -1
- package/src/onboarding/doctor.ts +1 -1
- package/src/registry.ts +1 -1
- package/src/tests/acceptance-probe-kinds.test.ts +262 -0
- package/src/tests/plugin-manifest.test.ts +1 -1
- package/src/tests/session-handlers.test.ts +29 -0
- package/src/types/werkstatt-engine-shims.d.ts +0 -21
- package/src/types/werkstatt-shared-shims.d.ts +68 -147
- /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,7 @@ Packet 000 will add the portable `forge/program@1` control plane under `os/progr
|
|
|
18
18
|
| Module | Commands | Source |
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| `forgeCoreModule` | `create`, `doctor`, `upgrade`, `forge.agents.generate`, `scaffold`, `port.scaffold`, `skill.validate`, `skill.list`, `port.validate`, `profile.validate`, `dev`, `build`, `validate`, `pinned.validate`, `pinned.init`, `package.health`, `docs.archive`, `forge.autonomy.validate` | `os/core/` |
|
|
21
|
-
| `forgeRfcModule` | `rfc.list`, `rfc.validate`, `rfc.create`, etc. | `os/rfc/` |
|
|
21
|
+
| `forgeRfcModule` | `rfc.list`, `rfc.validate`, `rfc.create`, `rfc.verification.emit`, `rfc.verification.refresh`, etc. | `os/rfc/` |
|
|
22
22
|
| `forgeWorkflowModule` | `workflow.lint`, `workflow.list`, `workflow.amend.list` | `os/workflow/` |
|
|
23
23
|
| `forgeNamingModule` | `naming.convention.lint` | `os/naming/` |
|
|
24
24
|
| `forgeCompassModule` | `compass.inventory`, `compass.validate`, `compass.summary.trim`, etc. (8 commands). All compass commands accept `--workpiece <path>` for scoping to a mission workpiece directory (RFC-0617). | `os/compass/` |
|
|
@@ -48,6 +48,8 @@ Skill packs MAY declare additional Compass contract blocks beyond the built-in `
|
|
|
48
48
|
|
|
49
49
|
- **Verify `ecosystem.commit` output.** `ecosystem.commit` commits whatever is in the git index (staged files). If another agent staged files before your call, those files will be included in your commit instead of your own. After every `ecosystem.commit`, run `git show --stat HEAD` to verify the correct files were committed. If wrong files were committed, the missing files remain untracked/unstaged and must be committed separately.
|
|
50
50
|
- **Wrap `loadForgeConfig` in try/catch in handlers that run in test workspaces.** `loadForgeConfig` throws when `forge.yaml` does not exist at the workspace root. Handlers that may be called from test workspaces without a `forge.yaml` (e.g. `runCompassValidation`) must guard the call with try/catch and skip pack-dependent logic when config is unavailable.
|
|
51
|
+
- **Register new top-level directories in `NAMING_CONVENTION_IGNORED_TOP_LEVEL`.** When a new top-level directory appears in the repo (e.g. `.adr-locks`, `.devin`, `.forge`, `.rfc-locks`, `patches`, `storage`), add it to `NAMING_CONVENTION_IGNORED_TOP_LEVEL` in `os/naming/naming-convention.ts`. Otherwise `naming.convention.lint` reports "unknown top-level directory" errors.
|
|
52
|
+
- **Add tool-generated/imported/historical dirs to `NAMING_CONVENTION_EXEMPT_DIRS`.** Directories containing tool-generated files (e.g. `docs/performance` with timestamp-based names), imported specification documents (e.g. `docs/specs` with `01-PBP-System-Specification.md`), and archived RFCs (e.g. `docs/rfcs/archive`) should be added to `NAMING_CONVENTION_EXEMPT_DIRS` in `os/naming/naming-convention.ts` to avoid false-positive kebab-case violations on files whose naming is controlled by external tools or conventions.
|
|
51
53
|
|
|
52
54
|
## Program packet control plane (RFC-0856)
|
|
53
55
|
|
|
@@ -151,6 +153,30 @@ YAML plain scalar values that **start with a backtick** (`` ` ``) must be double
|
|
|
151
153
|
|
|
152
154
|
`rfc.implement.stamp` enforces RFC-IMP-02: every checked acceptance criterion (`- [x]`) MUST have an inline `(evidence: ...)` annotation. Parenthetical references without the `evidence:` keyword (e.g. `(promote.ts, 13 tests)`) do NOT satisfy the rule — the stamp fails with "checked criteria lack inline (evidence: ...) annotation". Always format as: `- [x] <criterion text> (evidence: <file paths, commands, or test counts>)`. Commit the annotated criteria before running `rfc.implement.stamp`.
|
|
153
155
|
|
|
156
|
+
## RFC acceptance probes: criterion binding and coverage (RFC-0997)
|
|
157
|
+
|
|
158
|
+
RFC-0997 binds acceptance probes to criteria via the `criterion: "AC-N"` field and adds three validation rules for post-cutoff RFCs (createdAt >= `2026-09-01`):
|
|
159
|
+
|
|
160
|
+
- **V-35**: every probe SHALL declare `criterion` referencing an existing `AC-N` id. Missing, malformed, or dangling `criterion` is a blocking error.
|
|
161
|
+
- **V-36**: every top-level checklist line (`- [ ]` / `- [x]`) in `## Acceptance criteria` SHALL start with a unique `AC-N:` identifier. Duplicate or missing identifiers are blocking errors.
|
|
162
|
+
- **V-37**: checked criteria evidence SHALL resolve — `probe:AC-N` must reference a criterion with at least one bound probe; `test:<path>` and `<path>:<line>` must reference an existing file. **Evidence annotations MUST NOT contain em-dashes (—) or nested quotes** — the V-37 parser breaks on `test: path — "description"` and reports the file as non-existent. **The `<path>` token MUST be the file path only — no AC-N suffixes, no descriptions, no extra text after the path.** The parser treats everything after `test:` until end-of-parenthesis as the file path; `test: packages/forge/os/rfc/file.test.ts AC-1` is reported as non-existent because the parser looks for a file named `packages/forge/os/rfc/file.test.ts AC-1`. Use simple `(evidence: test:packages/forge/src/tests/file.test.ts)` without em-dashes, nested quotes, or AC-N suffixes. Discovered during RFC-0998 and RFC-0999.
|
|
163
|
+
- **RFC-IMP-08**: `rfc.implement.stamp` blocks stamping for post-cutoff RFCs of kind `architecture`, `contract`, or `command` that declare zero acceptance probes. Policy and deprecation kinds are exempt.
|
|
164
|
+
- **Coverage report**: `rfc.validate --json` and `rfc.acceptance.run` emit a non-blocking `coverage` block per post-cutoff RFC with `totalCriteria`, `probeBackedCriteria`, `coverageRatio`, `uncoveredCriteria`, `unboundProbes`.
|
|
165
|
+
|
|
166
|
+
## RFC criteria content rules: phase separation, reject checklist, criterion versioning (RFC-1006)
|
|
167
|
+
|
|
168
|
+
RFC-1006 adds five validation rules for post-cutoff RFCs and ADRs (createdAt >= `2026-09-03`):
|
|
169
|
+
|
|
170
|
+
- **V-38**: `accepted`/`implemented` RFCs with unchecked `DR-N` items in `## Document readiness` section are blocking errors. Document readiness criteria (`DR-N`) are separated from system conformance criteria (`AC-N`).
|
|
171
|
+
- **V-39**: acceptance criteria containing `SHALL ... and ...` joining two verb phrases are non-atomic — split into separate criteria.
|
|
172
|
+
- **V-40**: acceptance criteria containing unbounded quantity triggers (`fast`, `scalable`, `low latency`, etc.) are blocking errors — state a specific number.
|
|
173
|
+
- **V-41**: acceptance criteria containing weasel verbs (`handle gracefully`, `robust`, `works correctly`, etc.) are blocking errors — replace with a specific observable behavior.
|
|
174
|
+
- **V-42**: malformed criterion supersession annotations (`> Superseded AC-N (YYYY-MM-DD): <reason>`) are blocking errors. Superseded criteria are excluded from the unchecked count.
|
|
175
|
+
|
|
176
|
+
V-39..V-41 also apply to post-cutoff `implemented` ADRs via `adr.validate`. Pre-cutoff RFCs and ADRs are exempt. The reject checklist is conservative — it uses closed trigger lists, not semantic analysis. See `packages/forge/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md` for the canonical authoring guide.
|
|
177
|
+
|
|
178
|
+
- **Section extractors in `validate-rules.ts` MUST call `stripFencedCodeBlocks(body)` before regex-matching markdown sections.** RFC documents contain fenced code blocks with example headings and checklists. Without stripping, extractors like `extractDocumentReadinessSection` and `extractAcceptanceCriteriaSection` match headings inside code blocks, producing false-positive validation errors (e.g. V-38 reporting unchecked criteria from a code block example). The `stripFencedCodeBlocks` helper removes all ` ```...``` ` blocks before extraction. Any new section extractor added to `validate-rules.ts` must follow this pattern.
|
|
179
|
+
|
|
154
180
|
## Re-entrant werkstatt locks (RFC-0616)
|
|
155
181
|
|
|
156
182
|
`acquireLock` and `releaseLock` in `os/werkstatt/handlers/lock.ts` are re-entrant by PID. When the same process re-acquires a lock it already holds, `acquireLock` increments the `depth` counter instead of throwing. `releaseLock` decrements `depth` and only deletes the lock file when `depth` reaches `1` or is `undefined`. The `depth` field is `.optional()` in `werkstattLockSchema` — old lock files without `depth` parse successfully and are treated as `depth=1` via `?? 1` fallbacks. Agents MUST NOT assume `acquireLock` always throws on an existing lock file — it only throws when a **different** live process holds the lock.
|
package/README.md
CHANGED
|
@@ -447,6 +447,99 @@ Forge provides the tools — `forge doctor`, `forge rfc.validate`, `forge skill.
|
|
|
447
447
|
|
|
448
448
|
---
|
|
449
449
|
|
|
450
|
+
## Skill packs
|
|
451
|
+
|
|
452
|
+
Skill packs let you create project-local skills under your own prefix, separate from Forge's portable `fo-` skills. This is useful when your project has domain-specific workflows that don't belong in Forge's portable skill set.
|
|
453
|
+
|
|
454
|
+
### Declaring a pack
|
|
455
|
+
|
|
456
|
+
Add a `skillPacks` entry to `forge.yaml`:
|
|
457
|
+
|
|
458
|
+
```yaml
|
|
459
|
+
skillPacks:
|
|
460
|
+
- prefix: wg
|
|
461
|
+
dir: packages/my-skills/skills
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- `prefix` — short identifier used as a skill name prefix (e.g. `wg-deploy`, `wg-content-check`). Cannot be `fo` (reserved for Forge).
|
|
465
|
+
- `dir` — directory containing your pack skills. Each skill is a subdirectory with a `SKILL.md` file.
|
|
466
|
+
|
|
467
|
+
### Pack manifest (`forge.plugin.yaml`)
|
|
468
|
+
|
|
469
|
+
Each pack directory must contain a `forge.plugin.yaml` manifest:
|
|
470
|
+
|
|
471
|
+
```yaml
|
|
472
|
+
id: my-pack
|
|
473
|
+
version: 1.0.0
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
- `id` — kebab-case identifier for the pack.
|
|
477
|
+
- `version` — semver version string.
|
|
478
|
+
|
|
479
|
+
`forge init` auto-creates this manifest if it's missing. You can also create it manually.
|
|
480
|
+
|
|
481
|
+
### Skill structure
|
|
482
|
+
|
|
483
|
+
Skills follow the same structure as Forge skills:
|
|
484
|
+
|
|
485
|
+
```
|
|
486
|
+
packages/my-skills/skills/
|
|
487
|
+
forge.plugin.yaml # pack manifest
|
|
488
|
+
wg-deploy/
|
|
489
|
+
SKILL.md # skill definition with frontmatter
|
|
490
|
+
wg-content-check/
|
|
491
|
+
SKILL.md
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Each `SKILL.md` has standardized frontmatter (name, description, category, concerns, dependsOn) — the same format as Forge's `fo-` skills.
|
|
495
|
+
|
|
496
|
+
### Validation commands
|
|
497
|
+
|
|
498
|
+
```sh
|
|
499
|
+
# Validate all pack manifests
|
|
500
|
+
pnpm exec forge plugin.validate
|
|
501
|
+
|
|
502
|
+
# List discovered packs
|
|
503
|
+
pnpm exec forge plugin.discover
|
|
504
|
+
|
|
505
|
+
# Validate individual skills (including pack skills)
|
|
506
|
+
pnpm exec forge skill.validate
|
|
507
|
+
|
|
508
|
+
# List all skills (pack skills annotated with pack:<prefix>)
|
|
509
|
+
pnpm exec forge skill.list
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Extension points (Compass contract blocks)
|
|
513
|
+
|
|
514
|
+
Packs can declare custom Compass contract blocks — source-file markers that `compass.validate` enforces. This lets your pack define its own `<MY_CONTRACT>` blocks with required tags:
|
|
515
|
+
|
|
516
|
+
```yaml
|
|
517
|
+
id: my-pack
|
|
518
|
+
version: 1.0.0
|
|
519
|
+
extensionPoints:
|
|
520
|
+
compass:
|
|
521
|
+
contract:
|
|
522
|
+
blocks:
|
|
523
|
+
- blockId: api-contract
|
|
524
|
+
requiredFor:
|
|
525
|
+
- "packages/my-pack/**/*.ts"
|
|
526
|
+
requiredTags:
|
|
527
|
+
- name: purpose
|
|
528
|
+
minWords: 3
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
`compass.validate` will emit `COMPASS-PLUGIN-01` (missing block), `COMPASS-PLUGIN-02` (missing required tag), and `COMPASS-PLUGIN-03` (tag below minWords) for files matching `requiredFor` that don't carry the block.
|
|
532
|
+
|
|
533
|
+
### Rules
|
|
534
|
+
|
|
535
|
+
- Pack skill names must start with the pack prefix (e.g. `wg-deploy` for prefix `wg`).
|
|
536
|
+
- Pack skills cannot use the `fo-` prefix (reserved for Forge).
|
|
537
|
+
- Pack skills cannot reference platform RFC/ADR ids or platform names.
|
|
538
|
+
- Pack skills may depend on Forge skills, but Forge skills may not depend on pack skills (preserves portability).
|
|
539
|
+
- If a pack skill name conflicts with a Forge skill name, the pack skill is skipped during sync.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
450
543
|
## Stack profiles
|
|
451
544
|
|
|
452
545
|
A stack profile defines the project scaffold: directory structure, dependencies, CI config, and first workspace. Choose a profile with `--profile` when creating a new project.
|
package/README.uk.md
CHANGED
|
@@ -438,6 +438,99 @@ Forge надає інструменти — `forge doctor`, `forge rfc.validate`
|
|
|
438
438
|
|
|
439
439
|
---
|
|
440
440
|
|
|
441
|
+
## Пакети навичок
|
|
442
|
+
|
|
443
|
+
Пакети навичок (skill packs) дозволяють створювати навички проєкту під власним префіксом, окремо від портативних `fo-` навичок Forge. Це корисно, коли ваш проєкт має домен-специфічні робочі процеси, які не належать до портативного набору Forge.
|
|
444
|
+
|
|
445
|
+
### Оголошення пакету
|
|
446
|
+
|
|
447
|
+
Додайте запис `skillPacks` у `forge.yaml`:
|
|
448
|
+
|
|
449
|
+
```yaml
|
|
450
|
+
skillPacks:
|
|
451
|
+
- prefix: wg
|
|
452
|
+
dir: packages/my-skills/skills
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
- `prefix` — короткий ідентифікатор, що використовується як префікс імені навички (наприклад `wg-deploy`, `wg-content-check`). Не може бути `fo` (зарезервовано для Forge).
|
|
456
|
+
- `dir` — директорія, що містить навички вашого пакету. Кожна навичка — піддиректорія з файлом `SKILL.md`.
|
|
457
|
+
|
|
458
|
+
### Маніфест пакету (`forge.plugin.yaml`)
|
|
459
|
+
|
|
460
|
+
Кожна директорія пакету повинна містити маніфест `forge.plugin.yaml`:
|
|
461
|
+
|
|
462
|
+
```yaml
|
|
463
|
+
id: my-pack
|
|
464
|
+
version: 1.0.0
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- `id` — ідентифікатор пакету в kebab-case.
|
|
468
|
+
- `version` — рядок версії у форматі semver.
|
|
469
|
+
|
|
470
|
+
`forge init` автоматично створює цей маніфест, якщо він відсутній. Ви також можете створити його вручну.
|
|
471
|
+
|
|
472
|
+
### Структура навичок
|
|
473
|
+
|
|
474
|
+
Навички мають ту саму структуру, що й навички Forge:
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
packages/my-skills/skills/
|
|
478
|
+
forge.plugin.yaml # маніфест пакету
|
|
479
|
+
wg-deploy/
|
|
480
|
+
SKILL.md # визначення навички з frontmatter
|
|
481
|
+
wg-content-check/
|
|
482
|
+
SKILL.md
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Кожен `SKILL.md` має стандартизований frontmatter (name, description, category, concerns, dependsOn) — той самий формат, що й у `fo-` навичок Forge.
|
|
486
|
+
|
|
487
|
+
### Команди валідації
|
|
488
|
+
|
|
489
|
+
```sh
|
|
490
|
+
# Валідувати всі маніфести пакетів
|
|
491
|
+
pnpm exec forge plugin.validate
|
|
492
|
+
|
|
493
|
+
# Перелічити виявлені пакети
|
|
494
|
+
pnpm exec forge plugin.discover
|
|
495
|
+
|
|
496
|
+
# Валідувати окремі навички (включно з навичками пакетів)
|
|
497
|
+
pnpm exec forge skill.validate
|
|
498
|
+
|
|
499
|
+
# Перелічити всі навички (навички пакетів позначені pack:<prefix>)
|
|
500
|
+
pnpm exec forge skill.list
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Точки розширення (блоки Compass-контракту)
|
|
504
|
+
|
|
505
|
+
Пакети можуть оголошувати власні блоки Compass-контракту — маркери у вихідних файлах, які `compass.validate` перевіряє. Це дозволяє вашому пакету визначати власні `<MY_CONTRACT>` блоки з обов'язковими тегами:
|
|
506
|
+
|
|
507
|
+
```yaml
|
|
508
|
+
id: my-pack
|
|
509
|
+
version: 1.0.0
|
|
510
|
+
extensionPoints:
|
|
511
|
+
compass:
|
|
512
|
+
contract:
|
|
513
|
+
blocks:
|
|
514
|
+
- blockId: api-contract
|
|
515
|
+
requiredFor:
|
|
516
|
+
- "packages/my-pack/**/*.ts"
|
|
517
|
+
requiredTags:
|
|
518
|
+
- name: purpose
|
|
519
|
+
minWords: 3
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
`compass.validate` видаватиме `COMPASS-PLUGIN-01` (відсутній блок), `COMPASS-PLUGIN-02` (відсутній обов'язковий тег) та `COMPASS-PLUGIN-03` (тег з меншою кількістю слів) для файлів, що відповідають `requiredFor`, але не містять блок.
|
|
523
|
+
|
|
524
|
+
### Правила
|
|
525
|
+
|
|
526
|
+
- Імена навичок пакету повинні починатися з префіксу пакету (наприклад `wg-deploy` для префіксу `wg`).
|
|
527
|
+
- Навички пакету не можуть використовувати префікс `fo-` (зарезервовано для Forge).
|
|
528
|
+
- Навички пакету не можуть посилатися на ідентифікатори платформених RFC/ADR або назви платформи.
|
|
529
|
+
- Навички пакету можуть залежати від навичок Forge, але навички Forge не можуть залежати від навичок пакету (зберігає портативність).
|
|
530
|
+
- Якщо ім'я навички пакету конфліктує з іменем навички Forge, навичка пакету пропускається під час синхронізації.
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
441
534
|
## Профілі стеку
|
|
442
535
|
|
|
443
536
|
Профіль стеку визначає каркас проєкту: структуру директорій, залежності, конфігурацію CI та перший робочий простір. Оберіть профіль прапорцем `--profile` під час створення проєкту.
|
|
@@ -44,3 +44,11 @@ Forces and trade-offs that led to the decision: alternatives considered, constra
|
|
|
44
44
|
## Evolution
|
|
45
45
|
|
|
46
46
|
How the decision may change: thresholds that trigger revisiting it, metrics to watch, and — for post-hoc ADRs — references to commits or PRs that implemented it.
|
|
47
|
+
|
|
48
|
+
## Acceptance criteria
|
|
49
|
+
|
|
50
|
+
<!-- Optional. If present, each criterion is a falsifiable claim about an observable artifact,
|
|
51
|
+
written in EARS form with a stable AC-N: identifier. See packages/forge/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md (RFC-0996).
|
|
52
|
+
When this section exists and the ADR is implemented, adr.validate enforces AV-17: all items must be checked with (evidence: ...). -->
|
|
53
|
+
|
|
54
|
+
- [ ] AC-1: THE <artifact> SHALL <observable property> (evidence: file: <path:line>)
|
|
@@ -230,3 +230,206 @@ describe("ADR-DIR-01: directory structure convention (RFC-0722)", () => {
|
|
|
230
230
|
}
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
|
+
|
|
234
|
+
const ADR_BODY_WITH_CRITERIA = `
|
|
235
|
+
# ADR-9998: Test ADR with criteria
|
|
236
|
+
|
|
237
|
+
## Context
|
|
238
|
+
|
|
239
|
+
Test context.
|
|
240
|
+
|
|
241
|
+
## Decision
|
|
242
|
+
|
|
243
|
+
Test decision.
|
|
244
|
+
|
|
245
|
+
## Consequences
|
|
246
|
+
|
|
247
|
+
Test consequences.
|
|
248
|
+
|
|
249
|
+
## Evolution
|
|
250
|
+
|
|
251
|
+
Test evolution.
|
|
252
|
+
|
|
253
|
+
## Acceptance criteria
|
|
254
|
+
|
|
255
|
+
- [ ] AC-1: THE system SHALL do X
|
|
256
|
+
- [ ] AC-2: THE system SHALL do Y
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
const ADR_BODY_WITH_CHECKED_CRITERIA_NO_EVIDENCE = `
|
|
260
|
+
# ADR-9997: Test ADR with checked criteria no evidence
|
|
261
|
+
|
|
262
|
+
## Context
|
|
263
|
+
|
|
264
|
+
Test context.
|
|
265
|
+
|
|
266
|
+
## Decision
|
|
267
|
+
|
|
268
|
+
Test decision.
|
|
269
|
+
|
|
270
|
+
## Consequences
|
|
271
|
+
|
|
272
|
+
Test consequences.
|
|
273
|
+
|
|
274
|
+
## Evolution
|
|
275
|
+
|
|
276
|
+
Test evolution.
|
|
277
|
+
|
|
278
|
+
## Acceptance criteria
|
|
279
|
+
|
|
280
|
+
- [x] AC-1: THE system SHALL do X
|
|
281
|
+
- [x] AC-2: THE system SHALL do Y
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
const ADR_BODY_WITH_CHECKED_CRITERIA_WITH_EVIDENCE = `
|
|
285
|
+
# ADR-9996: Test ADR with checked criteria with evidence
|
|
286
|
+
|
|
287
|
+
## Context
|
|
288
|
+
|
|
289
|
+
Test context.
|
|
290
|
+
|
|
291
|
+
## Decision
|
|
292
|
+
|
|
293
|
+
Test decision.
|
|
294
|
+
|
|
295
|
+
## Consequences
|
|
296
|
+
|
|
297
|
+
Test consequences.
|
|
298
|
+
|
|
299
|
+
## Evolution
|
|
300
|
+
|
|
301
|
+
Test evolution.
|
|
302
|
+
|
|
303
|
+
## Acceptance criteria
|
|
304
|
+
|
|
305
|
+
- [x] AC-1: THE system SHALL do X (evidence: test: src/test.test.ts)
|
|
306
|
+
- [x] AC-2: THE system SHALL do Y (evidence: file: src/index.ts:42)
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
function createAdrFilePostCutoff(
|
|
310
|
+
workspaceRoot: string,
|
|
311
|
+
id: string,
|
|
312
|
+
status: string,
|
|
313
|
+
body: string,
|
|
314
|
+
extraFm: Record<string, unknown> = {},
|
|
315
|
+
): void {
|
|
316
|
+
const adrDir = join(workspaceRoot, "docs", "adrs");
|
|
317
|
+
mkdirSync(adrDir, { recursive: true });
|
|
318
|
+
const slug = id.toLowerCase();
|
|
319
|
+
const fm = [
|
|
320
|
+
"---",
|
|
321
|
+
`id: ${id}`,
|
|
322
|
+
`title: "Test ADR"`,
|
|
323
|
+
`status: ${status}`,
|
|
324
|
+
`scope: package`,
|
|
325
|
+
`decider: human:test`,
|
|
326
|
+
`createdAt: 2026-09-15`,
|
|
327
|
+
`updatedAt: 2026-09-15`,
|
|
328
|
+
...Object.entries(extraFm).map(([k, v]) => `${k}: ${v}`),
|
|
329
|
+
"---",
|
|
330
|
+
"",
|
|
331
|
+
body,
|
|
332
|
+
].join("\n");
|
|
333
|
+
writeFileSync(join(adrDir, `${slug}-test.md`), fm);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
describe("AV-17: acceptance criteria completeness for implemented post-cutoff ADRs (RFC-0996)", () => {
|
|
337
|
+
test("AC-6: post-cutoff implemented ADR with unchecked criteria → AV-17 error", async () => {
|
|
338
|
+
const dir = mkdtempSync(join(tmpdir(), "av17-unchecked-"));
|
|
339
|
+
try {
|
|
340
|
+
createAdrFilePostCutoff(dir, "ADR-9998", "implemented", ADR_BODY_WITH_CRITERIA, {
|
|
341
|
+
implementedAt: "2026-09-16",
|
|
342
|
+
});
|
|
343
|
+
const violations = await runValidate(dir, "ADR-9998");
|
|
344
|
+
const av17 = filterRule(violations, "AV-17");
|
|
345
|
+
expect(av17).toHaveLength(1);
|
|
346
|
+
expect(av17[0]!.severity).toBe("error");
|
|
347
|
+
expect(av17[0]!.message).toContain("2 unchecked acceptance criteria");
|
|
348
|
+
expect(av17[0]!.message).toContain("implemented");
|
|
349
|
+
} finally {
|
|
350
|
+
rmSync(dir, { recursive: true, force: true });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("AC-7: post-cutoff implemented ADR with checked criteria but no evidence → AV-17 error", async () => {
|
|
355
|
+
const dir = mkdtempSync(join(tmpdir(), "av17-noevidence-"));
|
|
356
|
+
try {
|
|
357
|
+
createAdrFilePostCutoff(
|
|
358
|
+
dir,
|
|
359
|
+
"ADR-9997",
|
|
360
|
+
"implemented",
|
|
361
|
+
ADR_BODY_WITH_CHECKED_CRITERIA_NO_EVIDENCE,
|
|
362
|
+
{
|
|
363
|
+
implementedAt: "2026-09-16",
|
|
364
|
+
},
|
|
365
|
+
);
|
|
366
|
+
const violations = await runValidate(dir, "ADR-9997");
|
|
367
|
+
const av17 = filterRule(violations, "AV-17");
|
|
368
|
+
expect(av17).toHaveLength(2);
|
|
369
|
+
expect(av17.every((v) => v.severity === "error")).toBe(true);
|
|
370
|
+
expect(av17.every((v) => v.message.includes("without (evidence: ...)"))).toBe(true);
|
|
371
|
+
} finally {
|
|
372
|
+
rmSync(dir, { recursive: true, force: true });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("AC-8a: pre-cutoff implemented ADR with unchecked criteria → no AV-17", async () => {
|
|
377
|
+
const dir = mkdtempSync(join(tmpdir(), "av17-precutoff-"));
|
|
378
|
+
try {
|
|
379
|
+
createAdrFile(dir, "ADR-9998", "implemented", ADR_BODY_WITH_CRITERIA, {
|
|
380
|
+
implementedAt: "2026-01-15",
|
|
381
|
+
});
|
|
382
|
+
const violations = await runValidate(dir, "ADR-9998");
|
|
383
|
+
const av17 = filterRule(violations, "AV-17");
|
|
384
|
+
expect(av17).toHaveLength(0);
|
|
385
|
+
} finally {
|
|
386
|
+
rmSync(dir, { recursive: true, force: true });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("AC-8b: post-cutoff implemented ADR without criteria section → no AV-17", async () => {
|
|
391
|
+
const dir = mkdtempSync(join(tmpdir(), "av17-nosection-"));
|
|
392
|
+
try {
|
|
393
|
+
createAdrFilePostCutoff(dir, "ADR-9999", "implemented", ADR_BODY, {
|
|
394
|
+
implementedAt: "2026-09-16",
|
|
395
|
+
});
|
|
396
|
+
const violations = await runValidate(dir, "ADR-9999");
|
|
397
|
+
const av17 = filterRule(violations, "AV-17");
|
|
398
|
+
expect(av17).toHaveLength(0);
|
|
399
|
+
} finally {
|
|
400
|
+
rmSync(dir, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("AC-8c: post-cutoff accepted (not implemented) ADR with unchecked criteria → no AV-17", async () => {
|
|
405
|
+
const dir = mkdtempSync(join(tmpdir(), "av17-notimplemented-"));
|
|
406
|
+
try {
|
|
407
|
+
createAdrFilePostCutoff(dir, "ADR-9998", "accepted", ADR_BODY_WITH_CRITERIA);
|
|
408
|
+
const violations = await runValidate(dir, "ADR-9998");
|
|
409
|
+
const av17 = filterRule(violations, "AV-17");
|
|
410
|
+
expect(av17).toHaveLength(0);
|
|
411
|
+
} finally {
|
|
412
|
+
rmSync(dir, { recursive: true, force: true });
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("post-cutoff implemented ADR with all criteria checked and evidence → no AV-17", async () => {
|
|
417
|
+
const dir = mkdtempSync(join(tmpdir(), "av17-clean-"));
|
|
418
|
+
try {
|
|
419
|
+
createAdrFilePostCutoff(
|
|
420
|
+
dir,
|
|
421
|
+
"ADR-9996",
|
|
422
|
+
"implemented",
|
|
423
|
+
ADR_BODY_WITH_CHECKED_CRITERIA_WITH_EVIDENCE,
|
|
424
|
+
{
|
|
425
|
+
implementedAt: "2026-09-16",
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
const violations = await runValidate(dir, "ADR-9996");
|
|
429
|
+
const av17 = filterRule(violations, "AV-17");
|
|
430
|
+
expect(av17).toHaveLength(0);
|
|
431
|
+
} finally {
|
|
432
|
+
rmSync(dir, { recursive: true, force: true });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -35,8 +35,10 @@ import {
|
|
|
35
35
|
ADR_REQUIRED_SECTIONS,
|
|
36
36
|
ADR_SCOPES,
|
|
37
37
|
ADR_STATUSES,
|
|
38
|
+
ADR_ACCEPTANCE_CRITERIA_CUTOFF,
|
|
38
39
|
} from "../types.ts";
|
|
39
|
-
import { RFC_DIR } from "../../rfc/types.ts";
|
|
40
|
+
import { RFC_DIR, RFC_CRITERIA_CONTENT_CUTOFF } from "../../rfc/types.ts";
|
|
41
|
+
import { evaluateAcceptanceCriteria } from "../../rfc/handlers/validate-rules.ts";
|
|
40
42
|
|
|
41
43
|
async function loadRfcIds(workspaceRoot: string): Promise<Set<string>> {
|
|
42
44
|
const rfcIds = new Set<string>();
|
|
@@ -426,6 +428,57 @@ async function validateSingleAdr(
|
|
|
426
428
|
}
|
|
427
429
|
}
|
|
428
430
|
}
|
|
431
|
+
|
|
432
|
+
// AV-17: acceptance criteria completeness for implemented post-cutoff ADRs (RFC-0996)
|
|
433
|
+
if (status === "implemented" && createdAt >= ADR_ACCEPTANCE_CRITERIA_CUTOFF) {
|
|
434
|
+
const evaluation = evaluateAcceptanceCriteria(body);
|
|
435
|
+
if (evaluation.totalUnchecked > 0) {
|
|
436
|
+
addViolation(
|
|
437
|
+
adrId,
|
|
438
|
+
relFile,
|
|
439
|
+
"AV-17",
|
|
440
|
+
`${adrId} has ${evaluation.totalUnchecked} unchecked acceptance criteria in "## Acceptance criteria" section but status is "implemented".`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
for (const line of evaluation.checkedWithoutEvidence) {
|
|
444
|
+
const snippet = line.length > 80 ? line.slice(0, 77) + "..." : line;
|
|
445
|
+
addViolation(
|
|
446
|
+
adrId,
|
|
447
|
+
relFile,
|
|
448
|
+
"AV-17",
|
|
449
|
+
`${adrId} has checked criterion without (evidence: ...) annotation: "${snippet}".`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// V-39..V-41: reject checklist for post-cutoff implemented ADRs (RFC-1006)
|
|
455
|
+
if (status === "implemented" && createdAt >= RFC_CRITERIA_CONTENT_CUTOFF) {
|
|
456
|
+
const evaluation = evaluateAcceptanceCriteria(body);
|
|
457
|
+
for (const v of evaluation.nonAtomicViolations) {
|
|
458
|
+
addViolation(
|
|
459
|
+
adrId,
|
|
460
|
+
relFile,
|
|
461
|
+
"V-39",
|
|
462
|
+
`${adrId} acceptance criterion ${v.acId} is non-atomic (contains "and" joining two behaviors): "${v.line}". Split into separate criteria.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
for (const v of evaluation.unboundedQuantityViolations) {
|
|
466
|
+
addViolation(
|
|
467
|
+
adrId,
|
|
468
|
+
relFile,
|
|
469
|
+
"V-40",
|
|
470
|
+
`${adrId} acceptance criterion ${v.acId} contains unbounded quantity "${v.trigger}": state a specific number or reference the decision that will set it.`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
for (const v of evaluation.weaselVerbViolations) {
|
|
474
|
+
addViolation(
|
|
475
|
+
adrId,
|
|
476
|
+
relFile,
|
|
477
|
+
"V-41",
|
|
478
|
+
`${adrId} acceptance criterion ${v.acId} contains weasel verb "${v.trigger}": replace with a specific observable behavior.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
429
482
|
}
|
|
430
483
|
|
|
431
484
|
function execGitLog(workspaceRoot: string, args: string[]): Promise<string> {
|
package/os/adr/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ statuses, scopes, frontmatter shape, validation results, and list output.
|
|
|
16
16
|
<item>Post-refactor hardening: document that an ADR may be superseded by a broader RFC.</item>
|
|
17
17
|
<item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
|
|
18
18
|
<item>RFC-0727: add AdrImplementStamp types for atomic ADR status transition.</item>
|
|
19
|
+
<item>RFC-0996: add AV-17 acceptance criteria completeness check for implemented post-cutoff ADRs.</item>
|
|
19
20
|
</CHANGE_SUMMARY>
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -103,6 +104,12 @@ export const ADR_REQUIRED_SECTIONS = [
|
|
|
103
104
|
"Evolution",
|
|
104
105
|
] as const;
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* RFC-0996: cutoff date for AV-17 (acceptance criteria completeness on implemented ADRs).
|
|
109
|
+
* ADRs created on or after this date are subject to AV-17. Pre-cutoff ADRs are exempt.
|
|
110
|
+
*/
|
|
111
|
+
export const ADR_ACCEPTANCE_CRITERIA_CUTOFF = "2026-09-01";
|
|
112
|
+
|
|
106
113
|
export const ADR_KNOWN_KEYS: readonly string[] = [
|
|
107
114
|
"id",
|
|
108
115
|
"title",
|
|
@@ -48,9 +48,12 @@ function extractBlockContentForPlugin(source: string, tagName: string): string |
|
|
|
48
48
|
|
|
49
49
|
const INVENTORY_OUTPUT_PATH = "docs/compass-inventory.xml";
|
|
50
50
|
|
|
51
|
-
function getExpectedCommentSyntax(
|
|
51
|
+
function getExpectedCommentSyntax(
|
|
52
|
+
filePath: string,
|
|
53
|
+
): "block" | "hash" | "semicolon" | "html" | null {
|
|
52
54
|
if (filePath.endsWith(".gd")) return "hash";
|
|
53
55
|
if (filePath.endsWith(".tscn") || filePath.endsWith(".tres")) return "semicolon";
|
|
56
|
+
if (filePath.endsWith(".md")) return "html";
|
|
54
57
|
if (
|
|
55
58
|
filePath.endsWith(".ts") ||
|
|
56
59
|
filePath.endsWith(".tsx") ||
|
|
@@ -96,6 +99,13 @@ function checkCommentSyntax(source: string, filePath: string): string | null {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
} else if (syntax === "html") {
|
|
103
|
+
const blockStart = source.indexOf("<!--");
|
|
104
|
+
const blockEnd = source.lastIndexOf("-->");
|
|
105
|
+
const tagPos = source.indexOf("<MODULE_CONTRACT>");
|
|
106
|
+
if (blockStart === -1 || blockEnd === -1 || blockStart > tagPos || tagPos > blockEnd) {
|
|
107
|
+
return "MODULE_CONTRACT must be inside a <!-- ... --> HTML comment for .md files";
|
|
108
|
+
}
|
|
99
109
|
}
|
|
100
110
|
return null;
|
|
101
111
|
}
|
|
@@ -15,6 +15,7 @@ for Compass source-file inventory.</purpose>
|
|
|
15
15
|
<item>Post-refactor hardening: detect nested packages/os workspaces before deriving Compass layer and workspace name.</item>
|
|
16
16
|
<item>RFC-0556: moved canonical implementation from @warpgogol/site-kernel to @warpgogol/forge for autonomous mode.</item>
|
|
17
17
|
<item>Game extensions: added .cs, .tscn, .tres, .gd to SOURCE_EXTENSIONS; createCompassInventoryEntries now reads forge.yaml compass.fileExtensions at runtime and merges with hardcoded set.</item>
|
|
18
|
+
<item>Added .md to SOURCE_EXTENSIONS for SKILL.md Compass coverage; detectAuthoringStatus excludes non-SKILL.md markdown files.</item>
|
|
18
19
|
</CHANGE_SUMMARY>
|
|
19
20
|
*/
|
|
20
21
|
|
|
@@ -58,6 +59,7 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
58
59
|
".tscn",
|
|
59
60
|
".tres",
|
|
60
61
|
".gd",
|
|
62
|
+
".md",
|
|
61
63
|
]);
|
|
62
64
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
63
65
|
".git",
|
|
@@ -266,6 +268,7 @@ function detectLayer(relativePath: string): string {
|
|
|
266
268
|
)
|
|
267
269
|
return "test";
|
|
268
270
|
if (relativePath.startsWith("src/")) return "source";
|
|
271
|
+
if (relativePath.endsWith("SKILL.md")) return "skill";
|
|
269
272
|
return "other";
|
|
270
273
|
}
|
|
271
274
|
|
|
@@ -386,6 +389,13 @@ function detectAuthoringStatus(
|
|
|
386
389
|
};
|
|
387
390
|
}
|
|
388
391
|
|
|
392
|
+
if (relativePath.endsWith(".md") && !relativePath.endsWith("SKILL.md")) {
|
|
393
|
+
return {
|
|
394
|
+
authoringStatus: "excluded",
|
|
395
|
+
exclusionReason: "non-skill-markdown",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
389
399
|
const isRootFile = !relativePath.includes("/");
|
|
390
400
|
if (
|
|
391
401
|
isRootFile &&
|