@matteeh/osq 0.1.0 → 0.2.0
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 +415 -72
- package/dist/cli/approve.d.ts +13 -3
- package/dist/cli/approve.d.ts.map +1 -1
- package/dist/cli/approve.js +59 -4
- package/dist/cli/approve.js.map +1 -1
- package/dist/cli/bin.js +0 -0
- package/dist/cli/confirm.d.ts +21 -0
- package/dist/cli/confirm.d.ts.map +1 -0
- package/dist/cli/confirm.js +28 -0
- package/dist/cli/confirm.js.map +1 -0
- package/dist/cli/doctor.d.ts +13 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +23 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/done.d.ts +12 -0
- package/dist/cli/done.d.ts.map +1 -0
- package/dist/cli/done.js +26 -0
- package/dist/cli/done.js.map +1 -0
- package/dist/cli/inbox.d.ts +22 -0
- package/dist/cli/inbox.d.ts.map +1 -0
- package/dist/cli/inbox.js +41 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +127 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +9 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/lint.d.ts +26 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/lint.js +60 -0
- package/dist/cli/lint.js.map +1 -0
- package/dist/cli/migrate.d.ts +18 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +42 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/new.js +1 -1
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/plan-queue.d.ts +50 -0
- package/dist/cli/plan-queue.d.ts.map +1 -0
- package/dist/cli/plan-queue.js +167 -0
- package/dist/cli/plan-queue.js.map +1 -0
- package/dist/cli/plan.d.ts +35 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +153 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/queue.d.ts +9 -0
- package/dist/cli/queue.d.ts.map +1 -0
- package/dist/cli/queue.js +24 -0
- package/dist/cli/queue.js.map +1 -0
- package/dist/cli/reject.d.ts +8 -0
- package/dist/cli/reject.d.ts.map +1 -0
- package/dist/cli/reject.js +18 -0
- package/dist/cli/reject.js.map +1 -0
- package/dist/cli/report.d.ts +1 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +141 -14
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/retry.d.ts +7 -0
- package/dist/cli/retry.d.ts.map +1 -0
- package/dist/cli/retry.js +29 -0
- package/dist/cli/retry.js.map +1 -0
- package/dist/cli/serve.d.ts +30 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +136 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +7 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/show.d.ts +2 -1
- package/dist/cli/show.d.ts.map +1 -1
- package/dist/cli/show.js +20 -3
- package/dist/cli/show.js.map +1 -1
- package/dist/cli/status.d.ts +1 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +2 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/watch.d.ts +9 -6
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +15 -2
- package/dist/cli/watch.js.map +1 -1
- package/dist/core/foundation/config-codex.d.ts +22 -0
- package/dist/core/foundation/config-codex.d.ts.map +1 -0
- package/dist/core/foundation/config-codex.js +58 -0
- package/dist/core/foundation/config-codex.js.map +1 -0
- package/dist/core/foundation/config-doctor.d.ts +14 -0
- package/dist/core/foundation/config-doctor.d.ts.map +1 -0
- package/dist/core/foundation/config-doctor.js +29 -0
- package/dist/core/foundation/config-doctor.js.map +1 -0
- package/dist/core/foundation/config-env.d.ts +3 -0
- package/dist/core/foundation/config-env.d.ts.map +1 -0
- package/dist/core/foundation/config-env.js +22 -0
- package/dist/core/foundation/config-env.js.map +1 -0
- package/dist/core/foundation/config-gates.d.ts +22 -0
- package/dist/core/foundation/config-gates.d.ts.map +1 -0
- package/dist/core/foundation/config-gates.js +43 -0
- package/dist/core/foundation/config-gates.js.map +1 -0
- package/dist/core/foundation/config-pi.d.ts +64 -0
- package/dist/core/foundation/config-pi.d.ts.map +1 -0
- package/dist/core/foundation/config-pi.js +131 -0
- package/dist/core/foundation/config-pi.js.map +1 -0
- package/dist/core/foundation/config-planning.d.ts +24 -0
- package/dist/core/foundation/config-planning.d.ts.map +1 -0
- package/dist/core/foundation/config-planning.js +60 -0
- package/dist/core/foundation/config-planning.js.map +1 -0
- package/dist/core/foundation/config-queue.d.ts +15 -0
- package/dist/core/foundation/config-queue.d.ts.map +1 -0
- package/dist/core/foundation/config-queue.js +24 -0
- package/dist/core/foundation/config-queue.js.map +1 -0
- package/dist/core/foundation/config-serve.d.ts +20 -0
- package/dist/core/foundation/config-serve.d.ts.map +1 -0
- package/dist/core/foundation/config-serve.js +35 -0
- package/dist/core/foundation/config-serve.js.map +1 -0
- package/dist/core/{config.d.ts → foundation/config.d.ts} +34 -3
- package/dist/core/foundation/config.d.ts.map +1 -0
- package/dist/core/{config.js → foundation/config.js} +35 -18
- package/dist/core/foundation/config.js.map +1 -0
- package/dist/core/foundation/doctor-managed.d.ts +13 -0
- package/dist/core/foundation/doctor-managed.d.ts.map +1 -0
- package/dist/core/foundation/doctor-managed.js +77 -0
- package/dist/core/foundation/doctor-managed.js.map +1 -0
- package/dist/core/foundation/doctor.d.ts +19 -0
- package/dist/core/foundation/doctor.d.ts.map +1 -0
- package/dist/core/foundation/doctor.js +187 -0
- package/dist/core/foundation/doctor.js.map +1 -0
- package/dist/core/foundation/harness-catalog.d.ts +96 -0
- package/dist/core/foundation/harness-catalog.d.ts.map +1 -0
- package/dist/core/foundation/harness-catalog.js +182 -0
- package/dist/core/foundation/harness-catalog.js.map +1 -0
- package/dist/core/foundation/init-blocks.d.ts +33 -0
- package/dist/core/foundation/init-blocks.d.ts.map +1 -0
- package/dist/core/foundation/init-blocks.js +165 -0
- package/dist/core/foundation/init-blocks.js.map +1 -0
- package/dist/core/foundation/init-managed.d.ts +10 -0
- package/dist/core/foundation/init-managed.d.ts.map +1 -0
- package/dist/core/foundation/init-managed.js +115 -0
- package/dist/core/foundation/init-managed.js.map +1 -0
- package/dist/core/foundation/init.d.ts +18 -0
- package/dist/core/foundation/init.d.ts.map +1 -0
- package/dist/core/foundation/init.js +113 -0
- package/dist/core/foundation/init.js.map +1 -0
- package/dist/core/{logger.d.ts → foundation/logger.d.ts} +1 -0
- package/dist/core/foundation/logger.d.ts.map +1 -0
- package/dist/core/{logger.js → foundation/logger.js} +18 -2
- package/dist/core/foundation/logger.js.map +1 -0
- package/dist/core/{new.d.ts → foundation/new.d.ts} +3 -0
- package/dist/core/foundation/new.d.ts.map +1 -0
- package/dist/core/foundation/new.js +206 -0
- package/dist/core/foundation/new.js.map +1 -0
- package/dist/core/foundation/package-root.d.ts +9 -0
- package/dist/core/foundation/package-root.d.ts.map +1 -0
- package/dist/core/foundation/package-root.js +11 -0
- package/dist/core/foundation/package-root.js.map +1 -0
- package/dist/core/lifecycle/done.d.ts +17 -0
- package/dist/core/lifecycle/done.d.ts.map +1 -0
- package/dist/core/lifecycle/done.js +97 -0
- package/dist/core/lifecycle/done.js.map +1 -0
- package/dist/core/lifecycle/reject.d.ts +26 -0
- package/dist/core/lifecycle/reject.d.ts.map +1 -0
- package/dist/core/lifecycle/reject.js +83 -0
- package/dist/core/lifecycle/reject.js.map +1 -0
- package/dist/core/lifecycle/retry-transition.d.ts +23 -0
- package/dist/core/lifecycle/retry-transition.d.ts.map +1 -0
- package/dist/core/lifecycle/retry-transition.js +41 -0
- package/dist/core/lifecycle/retry-transition.js.map +1 -0
- package/dist/core/lifecycle/retry.d.ts +18 -0
- package/dist/core/lifecycle/retry.d.ts.map +1 -0
- package/dist/core/lifecycle/retry.js +179 -0
- package/dist/core/lifecycle/retry.js.map +1 -0
- package/dist/core/report/approval-flags.d.ts +29 -0
- package/dist/core/report/approval-flags.d.ts.map +1 -0
- package/dist/core/report/approval-flags.js +138 -0
- package/dist/core/report/approval-flags.js.map +1 -0
- package/dist/core/report/planning-economics.d.ts +39 -0
- package/dist/core/report/planning-economics.d.ts.map +1 -0
- package/dist/core/report/planning-economics.js +168 -0
- package/dist/core/report/planning-economics.js.map +1 -0
- package/dist/core/report/planning-observed.d.ts +59 -0
- package/dist/core/report/planning-observed.d.ts.map +1 -0
- package/dist/core/report/planning-observed.js +135 -0
- package/dist/core/report/planning-observed.js.map +1 -0
- package/dist/core/report/planning-records.d.ts +69 -0
- package/dist/core/report/planning-records.d.ts.map +1 -0
- package/dist/core/report/planning-records.js +149 -0
- package/dist/core/report/planning-records.js.map +1 -0
- package/dist/core/report/planning-slice-lookup.d.ts +13 -0
- package/dist/core/report/planning-slice-lookup.d.ts.map +1 -0
- package/dist/core/report/planning-slice-lookup.js +146 -0
- package/dist/core/report/planning-slice-lookup.js.map +1 -0
- package/dist/core/report/planning-slice-measures.d.ts +17 -0
- package/dist/core/report/planning-slice-measures.d.ts.map +1 -0
- package/dist/core/report/planning-slice-measures.js +85 -0
- package/dist/core/report/planning-slice-measures.js.map +1 -0
- package/dist/core/report/planning-slice-record.d.ts +4 -0
- package/dist/core/report/planning-slice-record.d.ts.map +1 -0
- package/dist/core/report/planning-slice-record.js +66 -0
- package/dist/core/report/planning-slice-record.js.map +1 -0
- package/dist/core/report/planning-slice-turns.d.ts +13 -0
- package/dist/core/report/planning-slice-turns.d.ts.map +1 -0
- package/dist/core/report/planning-slice-turns.js +41 -0
- package/dist/core/report/planning-slice-turns.js.map +1 -0
- package/dist/core/report/planning-slice.d.ts +70 -0
- package/dist/core/report/planning-slice.d.ts.map +1 -0
- package/dist/core/report/planning-slice.js +147 -0
- package/dist/core/report/planning-slice.js.map +1 -0
- package/dist/core/report/planning.d.ts +31 -0
- package/dist/core/report/planning.d.ts.map +1 -0
- package/dist/core/report/planning.js +75 -0
- package/dist/core/report/planning.js.map +1 -0
- package/dist/core/report/report-events.d.ts +52 -0
- package/dist/core/report/report-events.d.ts.map +1 -0
- package/dist/core/report/report-events.js +198 -0
- package/dist/core/report/report-events.js.map +1 -0
- package/dist/core/report/report-pre-spawn.d.ts +25 -0
- package/dist/core/report/report-pre-spawn.d.ts.map +1 -0
- package/dist/core/report/report-pre-spawn.js +40 -0
- package/dist/core/report/report-pre-spawn.js.map +1 -0
- package/dist/core/report/report-retries.d.ts +30 -0
- package/dist/core/report/report-retries.d.ts.map +1 -0
- package/dist/core/report/report-retries.js +100 -0
- package/dist/core/report/report-retries.js.map +1 -0
- package/dist/core/report/report.d.ts +276 -0
- package/dist/core/report/report.d.ts.map +1 -0
- package/dist/core/report/report.js +1421 -0
- package/dist/core/report/report.js.map +1 -0
- package/dist/core/{lock.d.ts → run/lock.d.ts} +6 -0
- package/dist/core/run/lock.d.ts.map +1 -0
- package/dist/core/{lock.js → run/lock.js} +7 -15
- package/dist/core/run/lock.js.map +1 -0
- package/dist/core/run/manifest.d.ts +39 -0
- package/dist/core/run/manifest.d.ts.map +1 -0
- package/dist/core/run/manifest.js +137 -0
- package/dist/core/run/manifest.js.map +1 -0
- package/dist/core/run/scope-hash.d.ts +71 -0
- package/dist/core/run/scope-hash.d.ts.map +1 -0
- package/dist/core/run/scope-hash.js +177 -0
- package/dist/core/run/scope-hash.js.map +1 -0
- package/dist/core/run/scope.d.ts +30 -0
- package/dist/core/run/scope.d.ts.map +1 -0
- package/dist/core/run/scope.js +193 -0
- package/dist/core/run/scope.js.map +1 -0
- package/dist/core/run/summary.d.ts +8 -0
- package/dist/core/run/summary.d.ts.map +1 -0
- package/dist/core/run/summary.js +25 -0
- package/dist/core/run/summary.js.map +1 -0
- package/dist/core/run/verification.d.ts +23 -0
- package/dist/core/run/verification.d.ts.map +1 -0
- package/dist/core/run/verification.js +78 -0
- package/dist/core/run/verification.js.map +1 -0
- package/dist/core/spec/approve.d.ts +31 -0
- package/dist/core/spec/approve.d.ts.map +1 -0
- package/dist/core/spec/approve.js +111 -0
- package/dist/core/spec/approve.js.map +1 -0
- package/dist/core/spec/delta.d.ts +77 -0
- package/dist/core/spec/delta.d.ts.map +1 -0
- package/dist/core/spec/delta.js +581 -0
- package/dist/core/spec/delta.js.map +1 -0
- package/dist/core/spec/digest-flags.d.ts +21 -0
- package/dist/core/spec/digest-flags.d.ts.map +1 -0
- package/dist/core/spec/digest-flags.js +194 -0
- package/dist/core/spec/digest-flags.js.map +1 -0
- package/dist/core/spec/digest-verify-starts.d.ts +5 -0
- package/dist/core/spec/digest-verify-starts.d.ts.map +1 -0
- package/dist/core/spec/digest-verify-starts.js +31 -0
- package/dist/core/spec/digest-verify-starts.js.map +1 -0
- package/dist/core/spec/digest.d.ts +42 -0
- package/dist/core/spec/digest.d.ts.map +1 -0
- package/dist/core/spec/digest.js +170 -0
- package/dist/core/spec/digest.js.map +1 -0
- package/dist/core/spec/hasher.d.ts +11 -0
- package/dist/core/spec/hasher.d.ts.map +1 -0
- package/dist/core/{hasher.js → spec/hasher.js} +27 -3
- package/dist/core/spec/hasher.js.map +1 -0
- package/dist/core/spec/linter.d.ts +79 -0
- package/dist/core/spec/linter.d.ts.map +1 -0
- package/dist/core/spec/linter.js +826 -0
- package/dist/core/spec/linter.js.map +1 -0
- package/dist/core/spec/migrate.d.ts +54 -0
- package/dist/core/spec/migrate.d.ts.map +1 -0
- package/dist/core/spec/migrate.js +297 -0
- package/dist/core/spec/migrate.js.map +1 -0
- package/dist/core/spec/openspec-version.d.ts +30 -0
- package/dist/core/spec/openspec-version.d.ts.map +1 -0
- package/dist/core/spec/openspec-version.js +118 -0
- package/dist/core/spec/openspec-version.js.map +1 -0
- package/dist/core/spec/parser.d.ts +82 -0
- package/dist/core/spec/parser.d.ts.map +1 -0
- package/dist/core/spec/parser.js +221 -0
- package/dist/core/spec/parser.js.map +1 -0
- package/dist/core/spec/verify-paths.d.ts +18 -0
- package/dist/core/spec/verify-paths.d.ts.map +1 -0
- package/dist/core/spec/verify-paths.js +139 -0
- package/dist/core/spec/verify-paths.js.map +1 -0
- package/dist/core/spec/verify-starts.d.ts +45 -0
- package/dist/core/spec/verify-starts.d.ts.map +1 -0
- package/dist/core/spec/verify-starts.js +61 -0
- package/dist/core/spec/verify-starts.js.map +1 -0
- package/dist/core/status/inbox-cursor.d.ts +10 -0
- package/dist/core/status/inbox-cursor.d.ts.map +1 -0
- package/dist/core/status/inbox-cursor.js +33 -0
- package/dist/core/status/inbox-cursor.js.map +1 -0
- package/dist/core/status/inbox-projection.d.ts +15 -0
- package/dist/core/status/inbox-projection.d.ts.map +1 -0
- package/dist/core/status/inbox-projection.js +20 -0
- package/dist/core/status/inbox-projection.js.map +1 -0
- package/dist/core/status/inbox-text.d.ts +4 -0
- package/dist/core/status/inbox-text.d.ts.map +1 -0
- package/dist/core/status/inbox-text.js +48 -0
- package/dist/core/status/inbox-text.js.map +1 -0
- package/dist/core/status/inbox.d.ts +53 -0
- package/dist/core/status/inbox.d.ts.map +1 -0
- package/dist/core/status/inbox.js +167 -0
- package/dist/core/status/inbox.js.map +1 -0
- package/dist/core/status/layout.d.ts +29 -0
- package/dist/core/status/layout.d.ts.map +1 -0
- package/dist/core/status/layout.js +77 -0
- package/dist/core/status/layout.js.map +1 -0
- package/dist/core/status/queue-parser.d.ts +13 -0
- package/dist/core/status/queue-parser.d.ts.map +1 -0
- package/dist/core/status/queue-parser.js +88 -0
- package/dist/core/status/queue-parser.js.map +1 -0
- package/dist/core/status/queue-planning.d.ts +62 -0
- package/dist/core/status/queue-planning.d.ts.map +1 -0
- package/dist/core/status/queue-planning.js +158 -0
- package/dist/core/status/queue-planning.js.map +1 -0
- package/dist/core/status/queue-report-detail.d.ts +50 -0
- package/dist/core/status/queue-report-detail.d.ts.map +1 -0
- package/dist/core/status/queue-report-detail.js +140 -0
- package/dist/core/status/queue-report-detail.js.map +1 -0
- package/dist/core/status/queue-report.d.ts +14 -0
- package/dist/core/status/queue-report.d.ts.map +1 -0
- package/dist/core/status/queue-report.js +41 -0
- package/dist/core/status/queue-report.js.map +1 -0
- package/dist/core/status/queue-state.d.ts +53 -0
- package/dist/core/status/queue-state.d.ts.map +1 -0
- package/dist/core/status/queue-state.js +167 -0
- package/dist/core/status/queue-state.js.map +1 -0
- package/dist/core/status/queue.d.ts +7 -0
- package/dist/core/status/queue.d.ts.map +1 -0
- package/dist/core/status/queue.js +4 -0
- package/dist/core/status/queue.js.map +1 -0
- package/dist/core/status/show.d.ts +102 -0
- package/dist/core/status/show.d.ts.map +1 -0
- package/dist/core/status/show.js +704 -0
- package/dist/core/status/show.js.map +1 -0
- package/dist/core/status/state.d.ts +56 -0
- package/dist/core/status/state.d.ts.map +1 -0
- package/dist/core/status/state.js +187 -0
- package/dist/core/status/state.js.map +1 -0
- package/dist/core/status/status.d.ts +23 -0
- package/dist/core/status/status.d.ts.map +1 -0
- package/dist/core/status/status.js +173 -0
- package/dist/core/status/status.js.map +1 -0
- package/dist/core/web/web-data-change.d.ts +10 -0
- package/dist/core/web/web-data-change.d.ts.map +1 -0
- package/dist/core/web/web-data-change.js +106 -0
- package/dist/core/web/web-data-change.js.map +1 -0
- package/dist/core/web/web-data-folders.d.ts +39 -0
- package/dist/core/web/web-data-folders.d.ts.map +1 -0
- package/dist/core/web/web-data-folders.js +125 -0
- package/dist/core/web/web-data-folders.js.map +1 -0
- package/dist/core/web/web-data-graph.d.ts +8 -0
- package/dist/core/web/web-data-graph.d.ts.map +1 -0
- package/dist/core/web/web-data-graph.js +113 -0
- package/dist/core/web/web-data-graph.js.map +1 -0
- package/dist/core/web/web-data-lifecycle.d.ts +43 -0
- package/dist/core/web/web-data-lifecycle.d.ts.map +1 -0
- package/dist/core/web/web-data-lifecycle.js +141 -0
- package/dist/core/web/web-data-lifecycle.js.map +1 -0
- package/dist/core/web/web-data-observations.d.ts +30 -0
- package/dist/core/web/web-data-observations.d.ts.map +1 -0
- package/dist/core/web/web-data-observations.js +181 -0
- package/dist/core/web/web-data-observations.js.map +1 -0
- package/dist/core/web/web-data-tokens.d.ts +29 -0
- package/dist/core/web/web-data-tokens.d.ts.map +1 -0
- package/dist/core/web/web-data-tokens.js +62 -0
- package/dist/core/web/web-data-tokens.js.map +1 -0
- package/dist/core/web/web-data-types.d.ts +156 -0
- package/dist/core/web/web-data-types.d.ts.map +1 -0
- package/dist/core/web/web-data-types.js +15 -0
- package/dist/core/web/web-data-types.js.map +1 -0
- package/dist/core/web/web-data.d.ts +10 -0
- package/dist/core/web/web-data.d.ts.map +1 -0
- package/dist/core/web/web-data.js +8 -0
- package/dist/core/web/web-data.js.map +1 -0
- package/dist/core/web/web-events.d.ts +54 -0
- package/dist/core/web/web-events.d.ts.map +1 -0
- package/dist/core/web/web-events.js +162 -0
- package/dist/core/web/web-events.js.map +1 -0
- package/dist/core/web/web-export.d.ts +18 -0
- package/dist/core/web/web-export.d.ts.map +1 -0
- package/dist/core/web/web-export.js +112 -0
- package/dist/core/web/web-export.js.map +1 -0
- package/dist/core/web/web-server.d.ts +34 -0
- package/dist/core/web/web-server.d.ts.map +1 -0
- package/dist/core/web/web-server.js +182 -0
- package/dist/core/web/web-server.js.map +1 -0
- package/dist/core/web/web-static.d.ts +20 -0
- package/dist/core/web/web-static.d.ts.map +1 -0
- package/dist/core/web/web-static.js +104 -0
- package/dist/core/web/web-static.js.map +1 -0
- package/dist/harness/{agy.d.ts → agy/agy.d.ts} +8 -6
- package/dist/harness/agy/agy.d.ts.map +1 -0
- package/dist/harness/{agy.js → agy/agy.js} +49 -34
- package/dist/harness/agy/agy.js.map +1 -0
- package/dist/harness/claude/claude-turns.d.ts +29 -0
- package/dist/harness/claude/claude-turns.d.ts.map +1 -0
- package/dist/harness/claude/claude-turns.js +162 -0
- package/dist/harness/claude/claude-turns.js.map +1 -0
- package/dist/harness/claude/claude-usage.d.ts +12 -0
- package/dist/harness/claude/claude-usage.d.ts.map +1 -0
- package/dist/harness/claude/claude-usage.js +144 -0
- package/dist/harness/claude/claude-usage.js.map +1 -0
- package/dist/harness/codex/codex-observe-usage.d.ts +6 -0
- package/dist/harness/codex/codex-observe-usage.d.ts.map +1 -0
- package/dist/harness/codex/codex-observe-usage.js +215 -0
- package/dist/harness/codex/codex-observe-usage.js.map +1 -0
- package/dist/harness/codex/codex-prompt.d.ts +17 -0
- package/dist/harness/codex/codex-prompt.d.ts.map +1 -0
- package/dist/harness/codex/codex-prompt.js +43 -0
- package/dist/harness/codex/codex-prompt.js.map +1 -0
- package/dist/harness/codex/codex-stream.d.ts +23 -0
- package/dist/harness/codex/codex-stream.d.ts.map +1 -0
- package/dist/harness/codex/codex-stream.js +182 -0
- package/dist/harness/codex/codex-stream.js.map +1 -0
- package/dist/harness/codex/codex-usage.d.ts +26 -0
- package/dist/harness/codex/codex-usage.d.ts.map +1 -0
- package/dist/harness/codex/codex-usage.js +122 -0
- package/dist/harness/codex/codex-usage.js.map +1 -0
- package/dist/harness/codex/codex.d.ts +16 -0
- package/dist/harness/codex/codex.d.ts.map +1 -0
- package/dist/harness/codex/codex.js +92 -0
- package/dist/harness/codex/codex.js.map +1 -0
- package/dist/harness/index.d.ts +11 -2
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +24 -9
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/mock.d.ts +5 -2
- package/dist/harness/mock.d.ts.map +1 -1
- package/dist/harness/mock.js +10 -11
- package/dist/harness/mock.js.map +1 -1
- package/dist/harness/opencode/opencode-observe-usage.d.ts +4 -0
- package/dist/harness/opencode/opencode-observe-usage.d.ts.map +1 -0
- package/dist/harness/opencode/opencode-observe-usage.js +100 -0
- package/dist/harness/opencode/opencode-observe-usage.js.map +1 -0
- package/dist/harness/opencode/opencode-usage.d.ts +24 -0
- package/dist/harness/opencode/opencode-usage.d.ts.map +1 -0
- package/dist/harness/opencode/opencode-usage.js +181 -0
- package/dist/harness/opencode/opencode-usage.js.map +1 -0
- package/dist/harness/{opencode.d.ts → opencode/opencode.d.ts} +10 -7
- package/dist/harness/opencode/opencode.d.ts.map +1 -0
- package/dist/harness/{opencode.js → opencode/opencode.js} +159 -70
- package/dist/harness/opencode/opencode.js.map +1 -0
- package/dist/harness/pi/pi-args.d.ts +9 -0
- package/dist/harness/pi/pi-args.d.ts.map +1 -0
- package/dist/harness/pi/pi-args.js +33 -0
- package/dist/harness/pi/pi-args.js.map +1 -0
- package/dist/harness/pi/pi-stream.d.ts +27 -0
- package/dist/harness/pi/pi-stream.d.ts.map +1 -0
- package/dist/harness/pi/pi-stream.js +176 -0
- package/dist/harness/pi/pi-stream.js.map +1 -0
- package/dist/harness/pi/pi-tools.d.ts +5 -0
- package/dist/harness/pi/pi-tools.d.ts.map +1 -0
- package/dist/harness/pi/pi-tools.js +31 -0
- package/dist/harness/pi/pi-tools.js.map +1 -0
- package/dist/harness/pi/pi.d.ts +11 -0
- package/dist/harness/pi/pi.d.ts.map +1 -0
- package/dist/harness/pi/pi.js +109 -0
- package/dist/harness/pi/pi.js.map +1 -0
- package/dist/harness/prompt.d.ts +11 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +105 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/types.d.ts +287 -8
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js +125 -0
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/watcher/archive-verify.d.ts +9 -0
- package/dist/watcher/archive-verify.d.ts.map +1 -0
- package/dist/watcher/archive-verify.js +80 -0
- package/dist/watcher/archive-verify.js.map +1 -0
- package/dist/watcher/archiver.d.ts +14 -2
- package/dist/watcher/archiver.d.ts.map +1 -1
- package/dist/watcher/archiver.js +112 -33
- package/dist/watcher/archiver.js.map +1 -1
- package/dist/watcher/attempt.d.ts +19 -0
- package/dist/watcher/attempt.d.ts.map +1 -0
- package/dist/watcher/attempt.js +94 -0
- package/dist/watcher/attempt.js.map +1 -0
- package/dist/watcher/auto-retry.d.ts +11 -0
- package/dist/watcher/auto-retry.d.ts.map +1 -0
- package/dist/watcher/auto-retry.js +180 -0
- package/dist/watcher/auto-retry.js.map +1 -0
- package/dist/watcher/build.d.ts +24 -0
- package/dist/watcher/build.d.ts.map +1 -0
- package/dist/watcher/build.js +169 -0
- package/dist/watcher/build.js.map +1 -0
- package/dist/watcher/change-verify.d.ts +25 -0
- package/dist/watcher/change-verify.d.ts.map +1 -0
- package/dist/watcher/change-verify.js +57 -0
- package/dist/watcher/change-verify.js.map +1 -0
- package/dist/watcher/dev.d.ts +51 -0
- package/dist/watcher/dev.d.ts.map +1 -0
- package/dist/watcher/dev.js +124 -0
- package/dist/watcher/dev.js.map +1 -0
- package/dist/watcher/failure-reason.d.ts +7 -0
- package/dist/watcher/failure-reason.d.ts.map +1 -0
- package/dist/watcher/failure-reason.js +2 -0
- package/dist/watcher/failure-reason.js.map +1 -0
- package/dist/watcher/fingerprint.d.ts +19 -0
- package/dist/watcher/fingerprint.d.ts.map +1 -0
- package/dist/watcher/fingerprint.js +53 -0
- package/dist/watcher/fingerprint.js.map +1 -0
- package/dist/watcher/heartbeat.d.ts +33 -0
- package/dist/watcher/heartbeat.d.ts.map +1 -0
- package/dist/watcher/heartbeat.js +135 -0
- package/dist/watcher/heartbeat.js.map +1 -0
- package/dist/watcher/lock.d.ts +10 -0
- package/dist/watcher/lock.d.ts.map +1 -0
- package/dist/watcher/lock.js +14 -0
- package/dist/watcher/lock.js.map +1 -0
- package/dist/watcher/loop.d.ts +26 -6
- package/dist/watcher/loop.d.ts.map +1 -1
- package/dist/watcher/loop.js +154 -37
- package/dist/watcher/loop.js.map +1 -1
- package/dist/watcher/measures.d.ts +46 -0
- package/dist/watcher/measures.d.ts.map +1 -0
- package/dist/watcher/measures.js +203 -0
- package/dist/watcher/measures.js.map +1 -0
- package/dist/watcher/outcome.d.ts +32 -0
- package/dist/watcher/outcome.d.ts.map +1 -0
- package/dist/watcher/outcome.js +127 -0
- package/dist/watcher/outcome.js.map +1 -0
- package/dist/watcher/regression.d.ts +43 -0
- package/dist/watcher/regression.d.ts.map +1 -0
- package/dist/watcher/regression.js +175 -0
- package/dist/watcher/regression.js.map +1 -0
- package/dist/watcher/runner.d.ts +6 -68
- package/dist/watcher/runner.d.ts.map +1 -1
- package/dist/watcher/runner.js +74 -523
- package/dist/watcher/runner.js.map +1 -1
- package/dist/watcher/spawn.d.ts +48 -0
- package/dist/watcher/spawn.d.ts.map +1 -0
- package/dist/watcher/spawn.js +130 -0
- package/dist/watcher/spawn.js.map +1 -0
- package/dist/watcher/task-verify.d.ts +59 -0
- package/dist/watcher/task-verify.d.ts.map +1 -0
- package/dist/watcher/task-verify.js +108 -0
- package/dist/watcher/task-verify.js.map +1 -0
- package/dist/watcher/verify.d.ts +44 -0
- package/dist/watcher/verify.d.ts.map +1 -0
- package/dist/watcher/verify.js +156 -0
- package/dist/watcher/verify.js.map +1 -0
- package/package.json +21 -9
- package/templates/PLANNER.md +77 -0
- package/templates/openspec/config.yaml +24 -0
- package/templates/openspec/schemas/osq/README.md +30 -0
- package/templates/openspec/schemas/osq/schema.yaml +133 -0
- package/templates/openspec/schemas/osq/templates/proposal.md +66 -0
- package/templates/openspec/schemas/osq/templates/spec.md +13 -0
- package/templates/openspec/schemas/osq/templates/tasks.md +11 -0
- package/templates/proposal.md +66 -0
- package/templates/tasks.md +5 -0
- package/ui/dist/assets/index-CZYeQ8la.js +9 -0
- package/ui/dist/assets/index-D6zVSjO7.css +1 -0
- package/ui/dist/index.html +14 -0
- package/dist/core/approve.d.ts +0 -11
- package/dist/core/approve.d.ts.map +0 -1
- package/dist/core/approve.js +0 -52
- package/dist/core/approve.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/hasher.d.ts +0 -4
- package/dist/core/hasher.d.ts.map +0 -1
- package/dist/core/hasher.js.map +0 -1
- package/dist/core/init.d.ts +0 -12
- package/dist/core/init.d.ts.map +0 -1
- package/dist/core/init.js +0 -151
- package/dist/core/init.js.map +0 -1
- package/dist/core/linter.d.ts +0 -8
- package/dist/core/linter.d.ts.map +0 -1
- package/dist/core/linter.js +0 -100
- package/dist/core/linter.js.map +0 -1
- package/dist/core/lock.d.ts.map +0 -1
- package/dist/core/lock.js.map +0 -1
- package/dist/core/logger.d.ts.map +0 -1
- package/dist/core/logger.js.map +0 -1
- package/dist/core/new.d.ts.map +0 -1
- package/dist/core/new.js +0 -79
- package/dist/core/new.js.map +0 -1
- package/dist/core/parser.d.ts +0 -32
- package/dist/core/parser.d.ts.map +0 -1
- package/dist/core/parser.js +0 -87
- package/dist/core/parser.js.map +0 -1
- package/dist/core/report.d.ts +0 -54
- package/dist/core/report.d.ts.map +0 -1
- package/dist/core/report.js +0 -475
- package/dist/core/report.js.map +0 -1
- package/dist/core/show.d.ts +0 -50
- package/dist/core/show.d.ts.map +0 -1
- package/dist/core/show.js +0 -374
- package/dist/core/show.js.map +0 -1
- package/dist/core/state.d.ts +0 -25
- package/dist/core/state.d.ts.map +0 -1
- package/dist/core/state.js +0 -180
- package/dist/core/state.js.map +0 -1
- package/dist/core/status.d.ts +0 -10
- package/dist/core/status.d.ts.map +0 -1
- package/dist/core/status.js +0 -106
- package/dist/core/status.js.map +0 -1
- package/dist/harness/agy.d.ts.map +0 -1
- package/dist/harness/agy.js.map +0 -1
- package/dist/harness/opencode.d.ts.map +0 -1
- package/dist/harness/opencode.js.map +0 -1
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolveScope } from '../run/scope.js';
|
|
5
|
+
import { getArchiveDir, getChangesDir, getRejectedDir } from '../status/layout.js';
|
|
6
|
+
import { compareNumericPrefix } from '../status/state.js';
|
|
7
|
+
import { DeltaMergeError, mergeDelta, parseDelta } from './delta.js';
|
|
8
|
+
import { isExcludedChangePath } from './hasher.js';
|
|
9
|
+
import { OPENSPEC_EXPECTED_VERSION, assessOpenSpecVersion, formatInRangeWarning, } from './openspec-version.js';
|
|
10
|
+
import { hasDeclaredWrites, parseFrontmatter, parseSpecMd, parseTaskMd, resolveChangeDoc, } from './parser.js';
|
|
11
|
+
import { listNamedPaths, missingNamedPaths, namesExistingRepositoryPath, tokenizeVerifyCommand, } from './verify-paths.js';
|
|
12
|
+
import { analyzeVerifyStarts, namedPathCoveredByScopes, } from './verify-starts.js';
|
|
13
|
+
export { OPENSPEC_EXPECTED_VERSION };
|
|
14
|
+
/** Remediation guidance citing ADR 004 for any validator pin violation. */
|
|
15
|
+
const OPENSPEC_PIN_REMEDIATION = 'Refer to ADR 004 (decisions/004-pinned-openspec-validator.md) and run: pnpm add -D @fission-ai/openspec@1.13.1';
|
|
16
|
+
/** Prefix applied to every finding surfaced by the OpenSpec validator. */
|
|
17
|
+
export const OPENSPEC_ERROR_PREFIX = 'openspec:';
|
|
18
|
+
/**
|
|
19
|
+
* ASCII control characters prohibited in authored change-folder files. Newline
|
|
20
|
+
* (`\n`, 0x0A) and tab (`\t`, 0x09) are permitted as structural Markdown
|
|
21
|
+
* whitespace; every other control character indicates a mangled write.
|
|
22
|
+
*/
|
|
23
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control character detector
|
|
24
|
+
const PROHIBITED_CONTROL_REGEX = /[\x00-\x08\x0B-\x1F\x7F]/;
|
|
25
|
+
/** Acceptance checkbox pattern; two or more on one line indicates fused lines. */
|
|
26
|
+
const ACCEPTANCE_CHECKBOX_REGEX = /[-*]\s*\[[ xX]\]/g;
|
|
27
|
+
/**
|
|
28
|
+
* Requirement names that begin with an imperative instruction verb. A living
|
|
29
|
+
* capability spec declares observable state; a delta requirement whose name
|
|
30
|
+
* starts with "update" or "document" is an instruction and is rejected.
|
|
31
|
+
*/
|
|
32
|
+
const INSTRUCTION_SHAPED_REQUIREMENT_REGEX = /^(?:update|document)/i;
|
|
33
|
+
/** The exact template verify sentinel planning must replace before approval. */
|
|
34
|
+
const PLACEHOLDER_VERIFY_COMMAND = 'node -e "process.exit(0)"';
|
|
35
|
+
/** The one find-it error for a missing or empty proposal `## Surface` section. */
|
|
36
|
+
const PROPOSAL_SURFACE_ERROR = 'proposal.md needs a ## Surface section: list the commands, flags, config keys, frontmatter fields, document sections, dead reasons, and event types this change adds, changes, or removes, or write None';
|
|
37
|
+
/** HTML comments are structural scaffolding, not declared surface text. */
|
|
38
|
+
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
39
|
+
/**
|
|
40
|
+
* Reads a proposal body's `## Surface` section content. The heading must occupy
|
|
41
|
+
* its own line; a mention inside prose does not count. Returns `null` when the
|
|
42
|
+
* section is absent.
|
|
43
|
+
*/
|
|
44
|
+
function readSurfaceSection(body) {
|
|
45
|
+
const match = /^##\s+Surface\s*$/m.exec(body);
|
|
46
|
+
if (!match) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const rest = body.slice(match.index + match[0].length);
|
|
50
|
+
const boundary = rest.search(/\n##(?!#)\s/);
|
|
51
|
+
return boundary === -1 ? rest : rest.slice(0, boundary);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* True when a proposal has no `## Surface` section, or the section holds only
|
|
55
|
+
* HTML comments and whitespace. Lint checks presence, never the declared text.
|
|
56
|
+
*/
|
|
57
|
+
function proposalSurfaceIsEmpty(body) {
|
|
58
|
+
const section = readSurfaceSection(body);
|
|
59
|
+
if (section === null) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return section.replace(HTML_COMMENT_REGEX, '').trim().length === 0;
|
|
63
|
+
}
|
|
64
|
+
/** Package managers whose direct or `run` invocations reference a script. */
|
|
65
|
+
const PACKAGE_MANAGER_BINARIES = new Set(['pnpm', 'npm', 'yarn', 'bun']);
|
|
66
|
+
/** Package-script verbs that introduce the referenced script name. */
|
|
67
|
+
const PACKAGE_RUN_VERBS = new Set(['run', 'run-script']);
|
|
68
|
+
/**
|
|
69
|
+
* Package-manager subcommands that are not package scripts. Recognizing these
|
|
70
|
+
* as scripts would turn an ordinary package-manager command into a spurious
|
|
71
|
+
* missing-script error, so they fall through to the generic path analysis.
|
|
72
|
+
*/
|
|
73
|
+
const PACKAGE_MANAGER_SUBCOMMANDS = new Set([
|
|
74
|
+
'add',
|
|
75
|
+
'audit',
|
|
76
|
+
'bin',
|
|
77
|
+
'cache',
|
|
78
|
+
'config',
|
|
79
|
+
'create',
|
|
80
|
+
'dedupe',
|
|
81
|
+
'dlx',
|
|
82
|
+
'doctor',
|
|
83
|
+
'env',
|
|
84
|
+
'exec',
|
|
85
|
+
'import',
|
|
86
|
+
'init',
|
|
87
|
+
'install',
|
|
88
|
+
'licenses',
|
|
89
|
+
'link',
|
|
90
|
+
'list',
|
|
91
|
+
'login',
|
|
92
|
+
'logout',
|
|
93
|
+
'ls',
|
|
94
|
+
'outdated',
|
|
95
|
+
'pack',
|
|
96
|
+
'patch',
|
|
97
|
+
'prune',
|
|
98
|
+
'publish',
|
|
99
|
+
'rebuild',
|
|
100
|
+
'remove',
|
|
101
|
+
'root',
|
|
102
|
+
'setup',
|
|
103
|
+
'store',
|
|
104
|
+
'unlink',
|
|
105
|
+
'uninstall',
|
|
106
|
+
'update',
|
|
107
|
+
'upgrade',
|
|
108
|
+
'version',
|
|
109
|
+
'whoami',
|
|
110
|
+
'why',
|
|
111
|
+
'workspace',
|
|
112
|
+
'workspaces',
|
|
113
|
+
]);
|
|
114
|
+
/**
|
|
115
|
+
* Recognize the template sentinel under the narrow normalizations the spec
|
|
116
|
+
* allows: surrounding and repeated ASCII whitespace, single or double quotes
|
|
117
|
+
* around the expression, `-e` or `--eval`, and an optional trailing semicolon.
|
|
118
|
+
*/
|
|
119
|
+
function isPlaceholderVerify(tokens) {
|
|
120
|
+
if (tokens.length !== 3 || tokens[0] !== 'node') {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (tokens[1] !== '-e' && tokens[1] !== '--eval') {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const expression = tokens[2].trim().replace(/;+$/, '').replace(/\s+/g, '');
|
|
127
|
+
return expression === 'process.exit(0)';
|
|
128
|
+
}
|
|
129
|
+
/** A shell chain operator; lint already rejects these for task verifies. */
|
|
130
|
+
function chainsVerifyCommand(command) {
|
|
131
|
+
return command.includes('&&') || command.includes(';') || command.includes('|');
|
|
132
|
+
}
|
|
133
|
+
/** Extract the referenced package script name from a recognized invocation. */
|
|
134
|
+
function extractPackageScript(tokens) {
|
|
135
|
+
const [bin, first, second] = tokens;
|
|
136
|
+
if (bin === undefined || !PACKAGE_MANAGER_BINARIES.has(bin)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (first === undefined || first.startsWith('-')) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (PACKAGE_RUN_VERBS.has(first)) {
|
|
143
|
+
if (second === undefined || second.startsWith('-')) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return second;
|
|
147
|
+
}
|
|
148
|
+
if (PACKAGE_MANAGER_SUBCOMMANDS.has(first)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return first;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Analyze one verify command read-only: detect the template sentinel, resolve a
|
|
155
|
+
* recognized package script against the root manifest, or fall back to an
|
|
156
|
+
* unresolved-target warning when no existing path or script is named.
|
|
157
|
+
*/
|
|
158
|
+
export async function analyzeVerifyCommand(projectRoot, command, packageScripts) {
|
|
159
|
+
const tokens = tokenizeVerifyCommand(command);
|
|
160
|
+
if (isPlaceholderVerify(tokens)) {
|
|
161
|
+
return { placeholder: true, packageScript: null, missingScript: null, unresolved: false };
|
|
162
|
+
}
|
|
163
|
+
const packageScript = extractPackageScript(tokens);
|
|
164
|
+
if (packageScript !== null) {
|
|
165
|
+
return {
|
|
166
|
+
placeholder: false,
|
|
167
|
+
packageScript,
|
|
168
|
+
missingScript: packageScripts.has(packageScript) ? null : packageScript,
|
|
169
|
+
unresolved: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
placeholder: false,
|
|
174
|
+
packageScript: null,
|
|
175
|
+
missingScript: null,
|
|
176
|
+
unresolved: !(await namesExistingRepositoryPath(projectRoot, tokens)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Read the root manifest's `scripts` keys once. A missing or malformed manifest
|
|
181
|
+
* deterministically yields an empty set rather than throwing, and the command
|
|
182
|
+
* itself is never executed.
|
|
183
|
+
*/
|
|
184
|
+
async function readRootPackageScripts(projectRoot) {
|
|
185
|
+
try {
|
|
186
|
+
const raw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8');
|
|
187
|
+
const parsed = JSON.parse(raw);
|
|
188
|
+
const scripts = parsed.scripts;
|
|
189
|
+
if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
|
|
190
|
+
return new Set(Object.keys(scripts));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Missing or malformed manifest: no known scripts.
|
|
195
|
+
}
|
|
196
|
+
return new Set();
|
|
197
|
+
}
|
|
198
|
+
function placeholderVerifyError(label) {
|
|
199
|
+
return `${label} verify is the template placeholder ${PLACEHOLDER_VERIFY_COMMAND}; replace it with a real command that verifies the completed change's final tree`;
|
|
200
|
+
}
|
|
201
|
+
function missingPackageScriptError(label, script) {
|
|
202
|
+
return `${label} verify references package script "${script}" that is not defined in the root package.json`;
|
|
203
|
+
}
|
|
204
|
+
function unresolvedVerifyWarning(label, command) {
|
|
205
|
+
return `${label} verify names neither an existing repository path nor a package script: "${command}"`;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Recursively list a change folder's authored files relative to `baseDir`,
|
|
209
|
+
* applying the shared root-relative exclusion predicate. `.run`, `.git`, and
|
|
210
|
+
* `.DS_Store` are excluded at any depth while the transient root
|
|
211
|
+
* `plan-prompt.md` is excluded without hiding a nested same-named authored file.
|
|
212
|
+
*/
|
|
213
|
+
async function collectArtifactFiles(dir, baseDir) {
|
|
214
|
+
let entries;
|
|
215
|
+
try {
|
|
216
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const files = [];
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const fullPath = path.join(dir, entry.name);
|
|
224
|
+
const relPath = path.relative(baseDir, fullPath).split(path.sep).join('/');
|
|
225
|
+
if (isExcludedChangePath(relPath)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
files.push(...(await collectArtifactFiles(fullPath, baseDir)));
|
|
230
|
+
}
|
|
231
|
+
else if (entry.isFile()) {
|
|
232
|
+
files.push(relPath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return files;
|
|
236
|
+
}
|
|
237
|
+
/** Report the first prohibited control character found in `content`. */
|
|
238
|
+
function scanControlCharacters(relPath, content, errors) {
|
|
239
|
+
const match = PROHIBITED_CONTROL_REGEX.exec(content);
|
|
240
|
+
if (!match) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const code = match[0].charCodeAt(0).toString(16).toUpperCase().padStart(2, '0');
|
|
244
|
+
errors.push(`File ${relPath} contains prohibited control character (0x${code})`);
|
|
245
|
+
}
|
|
246
|
+
/** Report any task line carrying two or more acceptance checkbox items. */
|
|
247
|
+
function scanFusedAcceptance(taskFile, content, errors) {
|
|
248
|
+
const lines = content.split('\n');
|
|
249
|
+
lines.forEach((line, index) => {
|
|
250
|
+
const count = line.match(ACCEPTANCE_CHECKBOX_REGEX)?.length ?? 0;
|
|
251
|
+
if (count > 1) {
|
|
252
|
+
errors.push(`Task in ${taskFile} contains fused acceptance lines on line ${index + 1}`);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async function pathExists(target) {
|
|
257
|
+
return fs
|
|
258
|
+
.stat(target)
|
|
259
|
+
.then(() => true)
|
|
260
|
+
.catch(() => false);
|
|
261
|
+
}
|
|
262
|
+
function resolveOpenSpecSettings(config) {
|
|
263
|
+
const extended = config;
|
|
264
|
+
return extended.openspec ?? {};
|
|
265
|
+
}
|
|
266
|
+
function resolveOpenSpecRoot(config) {
|
|
267
|
+
const paths = config.paths;
|
|
268
|
+
return paths.openspecRoot ?? 'openspec';
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Resolves the project-local OpenSpec binary. Only a binary inside the
|
|
272
|
+
* project's own `node_modules/.bin` (or an explicitly configured absolute
|
|
273
|
+
* path) is used; missing installs resolve to `null` so linting degrades
|
|
274
|
+
* gracefully rather than failing on projects without OpenSpec.
|
|
275
|
+
*/
|
|
276
|
+
export async function resolveOpenSpecBin(projectRoot, config) {
|
|
277
|
+
const binName = resolveOpenSpecSettings(config).bin?.trim() || 'openspec';
|
|
278
|
+
if (path.isAbsolute(binName) || binName.includes('/') || binName.includes('\\')) {
|
|
279
|
+
return (await pathExists(binName)) ? binName : null;
|
|
280
|
+
}
|
|
281
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', binName);
|
|
282
|
+
return (await pathExists(localBin)) ? localBin : null;
|
|
283
|
+
}
|
|
284
|
+
/** Reads the resolved `@fission-ai/openspec` version from the local install. */
|
|
285
|
+
export async function resolveOpenSpecVersion(projectRoot) {
|
|
286
|
+
const manifestPath = path.join(projectRoot, 'node_modules', '@fission-ai', 'openspec', 'package.json');
|
|
287
|
+
try {
|
|
288
|
+
const raw = await fs.readFile(manifestPath, 'utf8');
|
|
289
|
+
const manifest = JSON.parse(raw);
|
|
290
|
+
return manifest.version ?? null;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function messageOf(value) {
|
|
297
|
+
if (typeof value === 'string') {
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
if (!value || typeof value !== 'object') {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const record = value;
|
|
304
|
+
for (const key of ['message', 'error', 'description', 'title', 'label', 'detail']) {
|
|
305
|
+
const candidate = record[key];
|
|
306
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
307
|
+
return candidate;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function levelOf(value) {
|
|
313
|
+
if (value && typeof value === 'object') {
|
|
314
|
+
const record = value;
|
|
315
|
+
for (const key of ['severity', 'level', 'type', 'kind']) {
|
|
316
|
+
const candidate = record[key];
|
|
317
|
+
if (typeof candidate === 'string') {
|
|
318
|
+
const lowered = candidate.toLowerCase();
|
|
319
|
+
if (lowered.includes('warn') || lowered.includes('info')) {
|
|
320
|
+
return 'warning';
|
|
321
|
+
}
|
|
322
|
+
if (lowered.includes('error') || lowered.includes('fail')) {
|
|
323
|
+
return 'error';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return 'error';
|
|
329
|
+
}
|
|
330
|
+
const FINDING_CONTAINERS = [
|
|
331
|
+
['errors', 'error'],
|
|
332
|
+
['warnings', 'warning'],
|
|
333
|
+
['issues', undefined],
|
|
334
|
+
['findings', undefined],
|
|
335
|
+
['problems', undefined],
|
|
336
|
+
['validations', undefined],
|
|
337
|
+
['results', undefined],
|
|
338
|
+
['items', undefined],
|
|
339
|
+
];
|
|
340
|
+
/**
|
|
341
|
+
* Tolerantly extracts validation findings from `openspec validate --json`.
|
|
342
|
+
* Accepts a top-level array or an object exposing `issues`/`errors`/`warnings`
|
|
343
|
+
* (or similar) and classifies each finding by its error/warning severity.
|
|
344
|
+
*/
|
|
345
|
+
export function parseOpenSpecFindings(output) {
|
|
346
|
+
const errors = [];
|
|
347
|
+
const warnings = [];
|
|
348
|
+
const trimmed = output.trim();
|
|
349
|
+
if (!trimmed) {
|
|
350
|
+
return { errors, warnings };
|
|
351
|
+
}
|
|
352
|
+
let parsed;
|
|
353
|
+
try {
|
|
354
|
+
parsed = JSON.parse(trimmed);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return { errors, warnings };
|
|
358
|
+
}
|
|
359
|
+
const seen = new Set();
|
|
360
|
+
const push = (level, message) => {
|
|
361
|
+
const normalized = message.trim();
|
|
362
|
+
if (!normalized) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const key = `${level}:${normalized}`;
|
|
366
|
+
if (seen.has(key)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
seen.add(key);
|
|
370
|
+
(level === 'warning' ? warnings : errors).push(normalized);
|
|
371
|
+
};
|
|
372
|
+
const visit = (value, hint) => {
|
|
373
|
+
if (typeof value === 'string') {
|
|
374
|
+
push(hint ?? 'error', value);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(value)) {
|
|
378
|
+
for (const item of value) {
|
|
379
|
+
visit(item, hint);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!value || typeof value !== 'object') {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const record = value;
|
|
387
|
+
const message = messageOf(record);
|
|
388
|
+
if (message) {
|
|
389
|
+
push(hint ?? levelOf(record), message);
|
|
390
|
+
}
|
|
391
|
+
for (const [key, keyHint] of FINDING_CONTAINERS) {
|
|
392
|
+
if (key in record) {
|
|
393
|
+
visit(record[key], hint ?? keyHint);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
visit(parsed);
|
|
398
|
+
return { errors, warnings };
|
|
399
|
+
}
|
|
400
|
+
function execFileCapture(bin, args, options) {
|
|
401
|
+
return new Promise((resolve) => {
|
|
402
|
+
execFile(bin, args, options, (error, stdout, stderr) => {
|
|
403
|
+
const code = error
|
|
404
|
+
? typeof error.code === 'number'
|
|
405
|
+
? error.code
|
|
406
|
+
: 1
|
|
407
|
+
: 0;
|
|
408
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '', code });
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Maps a validator version assessment to the single finding it produces: none
|
|
414
|
+
* for the pin, one warning inside the declared peer range, and one error citing
|
|
415
|
+
* ADR 004 and the install command outside it.
|
|
416
|
+
*/
|
|
417
|
+
function versionFinding(assessment) {
|
|
418
|
+
if (assessment.status === 'in-range' && assessment.range !== null) {
|
|
419
|
+
return { warning: formatInRangeWarning(assessment.version, assessment.range) };
|
|
420
|
+
}
|
|
421
|
+
if (assessment.status === 'out-of-range') {
|
|
422
|
+
return {
|
|
423
|
+
error: `OpenSpec validator version ${assessment.version} differs from pinned ${OPENSPEC_EXPECTED_VERSION}. ${OPENSPEC_PIN_REMEDIATION}`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Runs the local OpenSpec validator for changes and specs. Validation failures
|
|
430
|
+
* are returned prefixed with `openspec:`. A missing binary or a version outside
|
|
431
|
+
* the declared peer range is an error that cites ADR 004 and the install
|
|
432
|
+
* command; a version inside the range that differs from the pin warns and cites
|
|
433
|
+
* ADR 005. Linting fails closed rather than silently degrading.
|
|
434
|
+
*/
|
|
435
|
+
export async function validateWithOpenSpec(projectRoot, config, options = {}) {
|
|
436
|
+
const logger = options.logger;
|
|
437
|
+
const bin = await resolveOpenSpecBin(projectRoot, config);
|
|
438
|
+
if (!bin) {
|
|
439
|
+
return {
|
|
440
|
+
errors: [
|
|
441
|
+
`OpenSpec validator binary is unavailable at node_modules/.bin/openspec. ${OPENSPEC_PIN_REMEDIATION}`,
|
|
442
|
+
],
|
|
443
|
+
warnings: [],
|
|
444
|
+
ran: false,
|
|
445
|
+
bin: null,
|
|
446
|
+
version: null,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const version = await resolveOpenSpecVersion(projectRoot);
|
|
450
|
+
const errors = [];
|
|
451
|
+
const warnings = [];
|
|
452
|
+
if (!version) {
|
|
453
|
+
errors.push(`OpenSpec validator version could not be resolved from node_modules/@fission-ai/openspec/package.json. ${OPENSPEC_PIN_REMEDIATION}`);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
logger?.info(`openspec version ${version}`);
|
|
457
|
+
const finding = versionFinding(await assessOpenSpecVersion(version));
|
|
458
|
+
if (finding.error !== undefined) {
|
|
459
|
+
errors.push(finding.error);
|
|
460
|
+
}
|
|
461
|
+
if (finding.warning !== undefined) {
|
|
462
|
+
warnings.push(finding.warning);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const timeout = config.timeouts.verifyTimeoutSeconds * 1000;
|
|
466
|
+
const commands = [
|
|
467
|
+
['validate', '--changes', '--strict', '--json', '--no-interactive'],
|
|
468
|
+
['validate', '--specs', '--strict', '--json', '--no-interactive'],
|
|
469
|
+
];
|
|
470
|
+
for (const args of commands) {
|
|
471
|
+
const outcome = await execFileCapture(bin, args, {
|
|
472
|
+
cwd: projectRoot,
|
|
473
|
+
env: { ...process.env, OPENSPEC_TELEMETRY: '0' },
|
|
474
|
+
timeout,
|
|
475
|
+
});
|
|
476
|
+
const findings = parseOpenSpecFindings(outcome.stdout);
|
|
477
|
+
for (const error of findings.errors) {
|
|
478
|
+
errors.push(`${OPENSPEC_ERROR_PREFIX} ${error}`);
|
|
479
|
+
}
|
|
480
|
+
for (const warning of findings.warnings) {
|
|
481
|
+
warnings.push(`${OPENSPEC_ERROR_PREFIX} ${warning}`);
|
|
482
|
+
}
|
|
483
|
+
if (outcome.code !== 0 && findings.errors.length === 0 && findings.warnings.length === 0) {
|
|
484
|
+
const detail = (outcome.stderr || outcome.stdout).trim().split('\n')[0];
|
|
485
|
+
errors.push(`${OPENSPEC_ERROR_PREFIX} ${args.join(' ')} exited with code ${outcome.code}${detail ? `: ${detail}` : ''}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return { errors, warnings, ran: true, bin, version };
|
|
489
|
+
}
|
|
490
|
+
async function checkDependencyExists(projectRoot, depId, config) {
|
|
491
|
+
const dirsToCheck = [
|
|
492
|
+
getChangesDir(config.paths.openspecRoot, projectRoot),
|
|
493
|
+
getArchiveDir(config.paths.openspecRoot, projectRoot),
|
|
494
|
+
// A rejected change is a historical change: the reference stays auditable
|
|
495
|
+
// even though it never satisfies dependency completion.
|
|
496
|
+
getRejectedDir(config.paths.openspecRoot, projectRoot),
|
|
497
|
+
];
|
|
498
|
+
const paddedDep = depId.padStart(3, '0');
|
|
499
|
+
for (const dir of dirsToCheck) {
|
|
500
|
+
try {
|
|
501
|
+
const entries = await fs.readdir(dir);
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
if (entry.startsWith(`${paddedDep}-`) || entry === paddedDep) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Directory may not exist yet
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
/** Returns the leading instruction verb in a requirement name, else null. */
|
|
515
|
+
function instructionVerbOf(name) {
|
|
516
|
+
const match = name.trim().match(INSTRUCTION_SHAPED_REQUIREMENT_REGEX);
|
|
517
|
+
return match ? match[0].toLowerCase() : null;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Reports delta requirements whose names are instruction-shaped. Deltas must
|
|
521
|
+
* define living capability requirements, so names starting with an imperative
|
|
522
|
+
* verb such as "update" or "document" are rejected with the capability and the
|
|
523
|
+
* offending requirement title.
|
|
524
|
+
*/
|
|
525
|
+
function scanInstructionShapedRequirements(capability, requirements, errors) {
|
|
526
|
+
for (const requirement of requirements) {
|
|
527
|
+
const verb = instructionVerbOf(requirement.name);
|
|
528
|
+
if (!verb) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
errors.push(`${OPENSPEC_ERROR_PREFIX} ${capability}: requirement "${requirement.name}" is instruction-shaped ("${verb}"); delta specs must define living capability requirements, not instructions`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Verifies that every delta operation targeting a requirement resolves against
|
|
536
|
+
* the living base spec at `openspec/specs/<capability>/spec.md`. Applying the
|
|
537
|
+
* delta through the deterministic merge engine reports unmatched MODIFIED,
|
|
538
|
+
* REMOVED, or RENAMED targets; ADDED-only deltas may create a new capability.
|
|
539
|
+
*/
|
|
540
|
+
export async function verifyDeltaTargets(projectRoot, folderPath, config) {
|
|
541
|
+
const errors = [];
|
|
542
|
+
const deltasDir = path.join(folderPath, 'specs');
|
|
543
|
+
let entries = [];
|
|
544
|
+
try {
|
|
545
|
+
entries = await fs.readdir(deltasDir, { withFileTypes: true });
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return errors;
|
|
549
|
+
}
|
|
550
|
+
const capabilities = entries
|
|
551
|
+
.filter((entry) => entry.isDirectory())
|
|
552
|
+
.map((entry) => entry.name)
|
|
553
|
+
.sort();
|
|
554
|
+
if (capabilities.length === 0) {
|
|
555
|
+
return errors;
|
|
556
|
+
}
|
|
557
|
+
const specsRoot = path.join(projectRoot, resolveOpenSpecRoot(config), 'specs');
|
|
558
|
+
for (const capability of capabilities) {
|
|
559
|
+
const deltaContent = await fs
|
|
560
|
+
.readFile(path.join(deltasDir, capability, 'spec.md'), 'utf8')
|
|
561
|
+
.catch(() => null);
|
|
562
|
+
if (deltaContent === null) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const baseContent = await fs
|
|
566
|
+
.readFile(path.join(specsRoot, capability, 'spec.md'), 'utf8')
|
|
567
|
+
.catch(() => null);
|
|
568
|
+
const delta = parseDelta(deltaContent);
|
|
569
|
+
scanInstructionShapedRequirements(capability, [...delta.added, ...delta.modified, ...delta.removed], errors);
|
|
570
|
+
try {
|
|
571
|
+
mergeDelta(baseContent, capability, delta);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error instanceof DeltaMergeError) {
|
|
575
|
+
errors.push(`${OPENSPEC_ERROR_PREFIX} ${capability}: ${error.message}`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return errors;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Compares each pair of tasks' resolved existing files and returns one
|
|
586
|
+
* deterministic warning per shared task-pair/file. Tasks are ordered by numeric
|
|
587
|
+
* filename with the established stable fallback, and paths within a pair are
|
|
588
|
+
* sorted, so directory enumeration order never affects the diagnostics.
|
|
589
|
+
* Declarations that resolve to no existing file are already excluded from
|
|
590
|
+
* `existingPaths`; the shared resolver deduplicated them per task, so a task
|
|
591
|
+
* never warns against itself.
|
|
592
|
+
*/
|
|
593
|
+
function collectOverlapWarnings(scopes) {
|
|
594
|
+
const ordered = [...scopes].sort((a, b) => compareNumericPrefix(a.taskFile, b.taskFile));
|
|
595
|
+
const warnings = [];
|
|
596
|
+
for (let left = 0; left < ordered.length; left += 1) {
|
|
597
|
+
for (let right = left + 1; right < ordered.length; right += 1) {
|
|
598
|
+
const other = new Set(ordered[right].existingPaths);
|
|
599
|
+
const shared = ordered[left].existingPaths
|
|
600
|
+
.filter((relativePath) => other.has(relativePath))
|
|
601
|
+
.sort();
|
|
602
|
+
for (const relativePath of shared) {
|
|
603
|
+
warnings.push(`Tasks ${ordered[left].taskNumber} and ${ordered[right].taskNumber} share resolved scope file ${relativePath}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return warnings;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Warns once per task whose resolved scope contains a `src/harness/` file but
|
|
611
|
+
* no resolved file under `tests/fixtures/events/`. Reuses the resolver
|
|
612
|
+
* projection already collected for overlap analysis instead of re-walking.
|
|
613
|
+
*/
|
|
614
|
+
function collectHarnessFixtureWarnings(scopes) {
|
|
615
|
+
const ordered = [...scopes].sort((a, b) => compareNumericPrefix(a.taskFile, b.taskFile));
|
|
616
|
+
const warnings = [];
|
|
617
|
+
for (const scope of ordered) {
|
|
618
|
+
const hasHarness = scope.existingPaths.some((entry) => entry.startsWith('src/harness/'));
|
|
619
|
+
const hasFixtures = scope.existingPaths.some((entry) => entry === 'tests/fixtures/events' || entry.startsWith('tests/fixtures/events/'));
|
|
620
|
+
if (hasHarness && !hasFixtures) {
|
|
621
|
+
warnings.push(`Task in ${scope.taskFile} resolves src/harness/ scope but no file under tests/fixtures/events/`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return warnings;
|
|
625
|
+
}
|
|
626
|
+
/** Test files governed by the `tests.modify` gate; mirrors the `tests/**` default. */
|
|
627
|
+
function isTestFilePath(relativePath) {
|
|
628
|
+
return relativePath === 'tests' || relativePath.startsWith('tests/');
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Reads `tests.modify` from raw task frontmatter so a non-boolean value can be
|
|
632
|
+
* rejected. `parseTaskMd` intentionally collapses invalid types to `false`;
|
|
633
|
+
* schema validation therefore lives here. The nested `tests: { modify: ... }`
|
|
634
|
+
* form takes precedence over the flat `tests.modify` key.
|
|
635
|
+
*/
|
|
636
|
+
function readTestsModifyDeclaration(content) {
|
|
637
|
+
const { data } = parseFrontmatter(content);
|
|
638
|
+
const nested = data.tests;
|
|
639
|
+
if (nested !== null && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
640
|
+
const record = nested;
|
|
641
|
+
if ('modify' in record) {
|
|
642
|
+
const value = record.modify;
|
|
643
|
+
return { present: true, valid: typeof value === 'boolean', value };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if ('tests.modify' in data) {
|
|
647
|
+
const value = data['tests.modify'];
|
|
648
|
+
return { present: true, valid: typeof value === 'boolean', value };
|
|
649
|
+
}
|
|
650
|
+
return { present: false, valid: true };
|
|
651
|
+
}
|
|
652
|
+
export async function lintChangeFolder(projectRoot, folderPath, config, options = {}) {
|
|
653
|
+
const errors = [];
|
|
654
|
+
const warnings = [];
|
|
655
|
+
const packageScripts = await readRootPackageScripts(projectRoot);
|
|
656
|
+
// Check: no authored change-folder file contains prohibited control characters
|
|
657
|
+
for (const relPath of await collectArtifactFiles(folderPath, folderPath)) {
|
|
658
|
+
const content = await fs.readFile(path.join(folderPath, relPath), 'utf8');
|
|
659
|
+
scanControlCharacters(relPath, content, errors);
|
|
660
|
+
}
|
|
661
|
+
const resolvedDoc = await resolveChangeDoc(folderPath);
|
|
662
|
+
if (!resolvedDoc) {
|
|
663
|
+
errors.push('proposal.md (or legacy spec.md) not found in change folder');
|
|
664
|
+
return { valid: false, errors, warnings };
|
|
665
|
+
}
|
|
666
|
+
const specContent = await fs.readFile(resolvedDoc.path, 'utf8');
|
|
667
|
+
const spec = parseSpecMd(specContent);
|
|
668
|
+
// Check: proposals must declare a change-level verify command
|
|
669
|
+
if (resolvedDoc.kind === 'proposal' && !spec.verify) {
|
|
670
|
+
errors.push('proposal.md must declare a verify command in frontmatter');
|
|
671
|
+
}
|
|
672
|
+
// Check: proposals must declare a non-empty ## Surface section; legacy spec.md
|
|
673
|
+
// change documents are exempt exactly as they are for the verify check.
|
|
674
|
+
if (resolvedDoc.kind === 'proposal' &&
|
|
675
|
+
proposalSurfaceIsEmpty(parseFrontmatter(specContent).body)) {
|
|
676
|
+
errors.push(PROPOSAL_SURFACE_ERROR);
|
|
677
|
+
}
|
|
678
|
+
// Check: the change-level verify participates in the same trust analysis
|
|
679
|
+
if (spec.verify) {
|
|
680
|
+
const docLabel = path.basename(resolvedDoc.path);
|
|
681
|
+
const analysis = await analyzeVerifyCommand(projectRoot, spec.verify, packageScripts);
|
|
682
|
+
if (analysis.placeholder) {
|
|
683
|
+
errors.push(placeholderVerifyError(docLabel));
|
|
684
|
+
}
|
|
685
|
+
else if (!chainsVerifyCommand(spec.verify)) {
|
|
686
|
+
if (analysis.missingScript !== null) {
|
|
687
|
+
errors.push(missingPackageScriptError(docLabel, analysis.missingScript));
|
|
688
|
+
}
|
|
689
|
+
else if (analysis.unresolved) {
|
|
690
|
+
warnings.push(unresolvedVerifyWarning(docLabel, spec.verify));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Check: features.writes is retired; delta specs are the sole writes declaration
|
|
695
|
+
if (hasDeclaredWrites(parseFrontmatter(specContent).data)) {
|
|
696
|
+
errors.push('features.writes is no longer supported in proposal frontmatter; write declarations are derived strictly from delta specs under specs/<capability>/spec.md');
|
|
697
|
+
}
|
|
698
|
+
// Check: contract tables count
|
|
699
|
+
if (spec.contractTablesCount > config.limits.maxContractTables) {
|
|
700
|
+
errors.push(`Contract has ${spec.contractTablesCount} tables (max allowed is ${config.limits.maxContractTables})`);
|
|
701
|
+
}
|
|
702
|
+
// Check: depends_on exists
|
|
703
|
+
for (const dep of spec.dependsOn) {
|
|
704
|
+
const exists = await checkDependencyExists(projectRoot, dep, config);
|
|
705
|
+
if (!exists) {
|
|
706
|
+
errors.push(`depends_on names missing change: ${dep}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Tasks checks
|
|
710
|
+
const tasksDir = path.join(folderPath, 'tasks');
|
|
711
|
+
let taskEntries = [];
|
|
712
|
+
try {
|
|
713
|
+
taskEntries = (await fs.readdir(tasksDir)).filter((e) => e.endsWith('.md'));
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
errors.push('tasks directory not found in change folder');
|
|
717
|
+
}
|
|
718
|
+
if (taskEntries.length === 0) {
|
|
719
|
+
errors.push('No task files found under tasks/');
|
|
720
|
+
}
|
|
721
|
+
const resolvedTaskScopes = [];
|
|
722
|
+
for (const taskFile of taskEntries) {
|
|
723
|
+
const taskPath = path.join(tasksDir, taskFile);
|
|
724
|
+
const taskContent = await fs.readFile(taskPath, 'utf8');
|
|
725
|
+
const task = parseTaskMd(taskContent);
|
|
726
|
+
// Check: acceptance items must each occupy their own line
|
|
727
|
+
scanFusedAcceptance(taskFile, taskContent, errors);
|
|
728
|
+
// Check: tests.modify, when declared, must be a boolean
|
|
729
|
+
const testsModify = readTestsModifyDeclaration(taskContent);
|
|
730
|
+
if (testsModify.present && !testsModify.valid) {
|
|
731
|
+
errors.push(`Task in ${taskFile} tests.modify must be a boolean (got ${JSON.stringify(testsModify.value)})`);
|
|
732
|
+
}
|
|
733
|
+
// Resolve the declared scope once through the shared deterministic resolver.
|
|
734
|
+
const resolved = task.scope.length > 0 ? await resolveScope(projectRoot, task.scope) : [];
|
|
735
|
+
const existingPaths = resolved
|
|
736
|
+
.filter((entry) => entry.absolutePath !== null)
|
|
737
|
+
.map((entry) => entry.relativePath);
|
|
738
|
+
const taskLabel = `Task in ${taskFile}`;
|
|
739
|
+
const analysis = task.verify
|
|
740
|
+
? await analyzeVerifyCommand(projectRoot, task.verify, packageScripts)
|
|
741
|
+
: null;
|
|
742
|
+
resolvedTaskScopes.push({
|
|
743
|
+
taskFile,
|
|
744
|
+
taskNumber: taskFile.replace(/\.md$/, ''),
|
|
745
|
+
existingPaths,
|
|
746
|
+
task,
|
|
747
|
+
verifyAnalysis: analysis,
|
|
748
|
+
});
|
|
749
|
+
// Check: touching an existing test file requires tests.modify: true
|
|
750
|
+
if (!task.testsModify && existingPaths.length > 0) {
|
|
751
|
+
const touched = existingPaths.filter(isTestFilePath).sort();
|
|
752
|
+
if (touched.length > 0) {
|
|
753
|
+
errors.push(`Task in ${taskFile} scope touches existing test files (${touched.join(', ')}) without tests.modify: true`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Check: verify command empty, placeholder, chained, or missing script
|
|
757
|
+
if (analysis === null) {
|
|
758
|
+
errors.push(`Task in ${taskFile} verify command is empty`);
|
|
759
|
+
}
|
|
760
|
+
else if (analysis.placeholder) {
|
|
761
|
+
errors.push(placeholderVerifyError(taskLabel));
|
|
762
|
+
}
|
|
763
|
+
else if (chainsVerifyCommand(task.verify)) {
|
|
764
|
+
errors.push(`Task in ${taskFile} verify chains commands ("${task.verify}")`);
|
|
765
|
+
}
|
|
766
|
+
else if (analysis.missingScript !== null) {
|
|
767
|
+
errors.push(missingPackageScriptError(taskLabel, analysis.missingScript));
|
|
768
|
+
}
|
|
769
|
+
// Check: acceptance checklist length
|
|
770
|
+
if (task.acceptance.length > config.limits.maxAcceptanceLines) {
|
|
771
|
+
errors.push(`Task in ${taskFile} acceptance lines (${task.acceptance.length}) exceeds limit ${config.limits.maxAcceptanceLines}`);
|
|
772
|
+
}
|
|
773
|
+
// Check: scope expands to more than maxScopeFiles
|
|
774
|
+
if (task.scope.length > config.limits.maxScopeFiles) {
|
|
775
|
+
errors.push(`Task in ${taskFile} scope specifies ${task.scope.length} patterns, exceeding limit ${config.limits.maxScopeFiles}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Check: harness scope should cover the event fixtures without failing lint
|
|
779
|
+
warnings.push(...collectHarnessFixtureWarnings(resolvedTaskScopes));
|
|
780
|
+
// Check: shared resolved files across tasks are reported as non-failing warnings
|
|
781
|
+
warnings.push(...collectOverlapWarnings(resolvedTaskScopes));
|
|
782
|
+
// Tasks are analyzed in numeric order so earlier scopes can cover a later
|
|
783
|
+
// task's new paths. The unresolved-target warning is suppressed when every
|
|
784
|
+
// missing named path is one the task's own or an earlier task's scope covers.
|
|
785
|
+
const orderedScopes = [...resolvedTaskScopes].sort((a, b) => compareNumericPrefix(a.taskFile, b.taskFile));
|
|
786
|
+
const earlierScopes = [];
|
|
787
|
+
for (const scope of orderedScopes) {
|
|
788
|
+
if (scope.verifyAnalysis?.unresolved && !chainsVerifyCommand(scope.task.verify)) {
|
|
789
|
+
const namedPaths = listNamedPaths(scope.task.verify);
|
|
790
|
+
const missingPaths = await missingNamedPaths(projectRoot, scope.task.verify);
|
|
791
|
+
const covered = namedPaths.length > 0 &&
|
|
792
|
+
missingPaths.every((namedPath) => namedPathCoveredByScopes(namedPath, [scope.task.scope, ...earlierScopes]));
|
|
793
|
+
if (!covered) {
|
|
794
|
+
warnings.push(unresolvedVerifyWarning(`Task in ${scope.taskFile}`, scope.task.verify));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
earlierScopes.push([...scope.task.scope]);
|
|
798
|
+
}
|
|
799
|
+
// Check: a task whose verify names a test it creates must declare red, and a
|
|
800
|
+
// named path no task scope up to this task covers cannot be created at all
|
|
801
|
+
const verifyStartTasks = orderedScopes.map((scope) => ({
|
|
802
|
+
taskNumber: scope.taskNumber,
|
|
803
|
+
verify: scope.task.verify,
|
|
804
|
+
verifyStarts: scope.task.verifyStarts,
|
|
805
|
+
scope: scope.task.scope,
|
|
806
|
+
}));
|
|
807
|
+
const verifyStarts = await analyzeVerifyStarts(projectRoot, verifyStartTasks);
|
|
808
|
+
for (const contradiction of verifyStarts.contradictions) {
|
|
809
|
+
warnings.push(`Task in ${contradiction.taskNumber}.md declares verify_starts: ${contradiction.start} but its verify names ${contradiction.path}, which the task creates`);
|
|
810
|
+
}
|
|
811
|
+
for (const uncreatable of verifyStarts.uncreatable) {
|
|
812
|
+
warnings.push(`Task in ${uncreatable.taskNumber}.md verify names ${uncreatable.path}, which no task in the change can create`);
|
|
813
|
+
}
|
|
814
|
+
// Check: delta targets resolve against living base specs before approval
|
|
815
|
+
errors.push(...(await verifyDeltaTargets(projectRoot, folderPath, config)));
|
|
816
|
+
// Check: pinned OpenSpec validator
|
|
817
|
+
const openSpec = await validateWithOpenSpec(projectRoot, config, options);
|
|
818
|
+
errors.push(...openSpec.errors);
|
|
819
|
+
warnings.push(...openSpec.warnings);
|
|
820
|
+
return {
|
|
821
|
+
valid: errors.length === 0,
|
|
822
|
+
errors,
|
|
823
|
+
warnings,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
//# sourceMappingURL=linter.js.map
|