create-agent-rig 0.9.1 → 0.10.1
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/CHANGELOG.md +462 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +508 -101
- package/packages/cli/dist/index.js +658 -47
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +45 -12
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +4 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +59 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +45 -12
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +112 -40
- package/templates/release-ledger.json +3 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- package/templates/skeleton/node-service/vitest.config.ts +0 -12
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The capability & degradation contract (RP-36): what each declared policy is
|
|
3
|
-
* worth on one harness surface, how that answer is established, and how it is
|
|
4
|
-
* maintained afterwards.
|
|
5
|
-
*
|
|
6
|
-
* ⚠ A LIBRARY, not a running contract. Nothing in this rig calls any of it
|
|
7
|
-
* yet: `doctor` rendering the coverage report is its own task, and so is the
|
|
8
|
-
* caller that would report observed traffic. The present tense below describes
|
|
9
|
-
* what the functions do when called, not something happening today. The
|
|
10
|
-
* rationale and the full list of what this deliberately does not do are in
|
|
11
|
-
* `docs/decisions/capability-coverage.md`.
|
|
12
|
-
*
|
|
13
|
-
* A status is established by `./probe.ts` on an occasion named from
|
|
14
|
-
* `./vocabulary.ts` (`PROBE_TRIGGERS`), and maintained from what real traffic
|
|
15
|
-
* was expected to show and did not. Three rules follow, and they are the whole
|
|
16
|
-
* design.
|
|
17
|
-
*
|
|
18
|
-
* 🔴 **A timer alone never degrades anything.** `observeExpectedSignal` takes
|
|
19
|
-
* an OBSERVATION, never an elapsed time, and this module exports no function
|
|
20
|
-
* that ages, expires or sweeps an entry — pinned by
|
|
21
|
-
* `packages/cli/test/policy-coverage.test.ts` › "exports nothing that ages,
|
|
22
|
-
* expires or sweeps an entry, so only an observation can move it", and
|
|
23
|
-
* measured end-to-end by › "reports no downgrade when a year passes and the
|
|
24
|
-
* same wiring is probed again, because elapsed time is not evidence" against
|
|
25
|
-
* its positive control. No traffic is not a failure: it is no evidence.
|
|
26
|
-
*
|
|
27
|
-
* 🔴 **Traffic lowers a status; it never raises one.** A missing signal is
|
|
28
|
-
* evidence the mechanism did not act. A present signal is evidence it acted
|
|
29
|
-
* once — not that a wiring defect the probe found has been repaired. So the
|
|
30
|
-
* only route back up is another probe: › "never raises a status on traffic: a
|
|
31
|
-
* degraded surface stays degraded until a probe says otherwise".
|
|
32
|
-
*
|
|
33
|
-
* 🔴 **Traffic that CONTRADICTS the record is its own answer.** A signal
|
|
34
|
-
* observed on a surface the map records as `UNSUPPORTED` is not a promotion
|
|
35
|
-
* and not a miss — the wiring and the traffic disagree, which is what
|
|
36
|
-
* `INTEGRATION-FAILED` names: › "a signal observed where the map says nothing
|
|
37
|
-
* is wired is a contradiction, not a pass". It is still not a pass;
|
|
38
|
-
* `qualifierFor` maps it to `UNVERIFIABLE`.
|
|
39
|
-
*
|
|
40
|
-
* **Why this module throws where its neighbours return a `Validation`.** The
|
|
41
|
-
* line is what the value IS, not how bad it is: outside data read off a disk
|
|
42
|
-
* becomes a RESULT — `snapshot: unknown` is never trusted and never throws, it
|
|
43
|
-
* becomes `INTEGRATION-FAILED` — while a value the CALLER chose (a word from a
|
|
44
|
-
* closed vocabulary, its own clock, its own claim about which build it saw) is
|
|
45
|
-
* a programming error and throws. `./evidence-matrix.ts` sits on the first
|
|
46
|
-
* side, this module's two entry points on the second.
|
|
47
|
-
*
|
|
48
|
-
* Two limits worth stating rather than leaving to be discovered. `DEGRADED`
|
|
49
|
-
* carries no verdict qualifier, so an operation on precisely the tool a
|
|
50
|
-
* degraded matcher lost does not require a qualifier from this library — the
|
|
51
|
-
* item scopes the `UNVERIFIABLE` requirement to the unenforceable states; the
|
|
52
|
-
* caller owns the operation verdict. And a `reason`
|
|
53
|
-
* supplied with a miss that does not cross the threshold is not retained;
|
|
54
|
-
* only the miss that degrades records one — › "discards the reason given with
|
|
55
|
-
* a miss that does not degrade, and records the one given with the miss that
|
|
56
|
-
* does".
|
|
57
|
-
*/
|
|
58
|
-
import { probePolicy } from './probe.js';
|
|
59
|
-
import { EXACT_VERSION_EXPECTED, isExactVersion, ISO_8601 } from './validation.js';
|
|
60
|
-
import { PROBE_TRIGGERS, UNENFORCEABLE_STATES } from './vocabulary.js';
|
|
61
|
-
/**
|
|
62
|
-
* How many consecutive expected-but-absent signals degrade a supported policy.
|
|
63
|
-
*
|
|
64
|
-
* Three, not one: a single miss is as easily an operation that never reached
|
|
65
|
-
* the mechanism as a mechanism that failed to act, and a contract that
|
|
66
|
-
* degrades on it reports noise. It is a DEFAULT and not a constant — every
|
|
67
|
-
* caller may set its own, because what counts as enough evidence depends on
|
|
68
|
-
* how much traffic the surface sees.
|
|
69
|
-
*/
|
|
70
|
-
export const DEFAULT_DEGRADATION_THRESHOLD = 3;
|
|
71
|
-
/**
|
|
72
|
-
* How much a state can enforce, as an order, for deciding what counts as a
|
|
73
|
-
* fall. `UNSUPPORTED` and `INTEGRATION-FAILED` rank together on purpose: both
|
|
74
|
-
* mean the policy cannot be relied on here, they differ in whether anything is
|
|
75
|
-
* wired at all, and calling a move between them a downgrade would report a
|
|
76
|
-
* change of diagnosis as a loss of capability.
|
|
77
|
-
*
|
|
78
|
-
* This is a DISPLAY ordering and nothing else reads it. What qualifies a
|
|
79
|
-
* verdict is `UNENFORCEABLE_STATES` in `./vocabulary.ts`, which
|
|
80
|
-
* `./decision-record.ts` reads too — keying qualification off a rank would
|
|
81
|
-
* mean a future re-rank silently changed which verdicts are UNVERIFIABLE.
|
|
82
|
-
*/
|
|
83
|
-
const ENFORCEMENT_RANK = {
|
|
84
|
-
SUPPORTED: 0,
|
|
85
|
-
DEGRADED: 1,
|
|
86
|
-
UNSUPPORTED: 2,
|
|
87
|
-
'INTEGRATION-FAILED': 2,
|
|
88
|
-
};
|
|
89
|
-
const requireTimestamp = (field, value) => {
|
|
90
|
-
if (!ISO_8601.test(value)) {
|
|
91
|
-
throw new Error(`${field} must be an ISO-8601 date-time with an explicit zone; got ${JSON.stringify(value)}`);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
/**
|
|
95
|
-
* The probe's answer for every policy, as one map.
|
|
96
|
-
*
|
|
97
|
-
* `trigger` is required, and that is the point: `PROBE_TRIGGERS` names only
|
|
98
|
-
* changes to the surface, so a caller must say which change occasioned this
|
|
99
|
-
* read and cannot name an interval. `at` is the caller's clock — nothing here
|
|
100
|
-
* reads one — and it is validated, because a status whose time cannot be
|
|
101
|
-
* ordered against a change is not auditable. The surface harness and path must
|
|
102
|
-
* also name the adapter doing the read; otherwise valid wiring from one
|
|
103
|
-
* harness could be labeled as support on another.
|
|
104
|
-
*/
|
|
105
|
-
export function coverageFromProbe(args) {
|
|
106
|
-
const { surface, policies, adapter, snapshot, at, trigger, evidencePointer } = args;
|
|
107
|
-
if (!PROBE_TRIGGERS.includes(trigger)) {
|
|
108
|
-
throw new Error(`a probe is occasioned by one of ${PROBE_TRIGGERS.join(', ')}; got ${JSON.stringify(trigger)}`);
|
|
109
|
-
}
|
|
110
|
-
requireTimestamp('at', at);
|
|
111
|
-
if (surface.harness !== adapter.harness || surface.surface !== adapter.surfaceFile) {
|
|
112
|
-
throw new Error(`surface identity must match the adapter being probed; expected ${JSON.stringify(adapter.harness)} at ${JSON.stringify(adapter.surfaceFile)}, got ${JSON.stringify(surface.harness)} at ${JSON.stringify(surface.surface)}`);
|
|
113
|
-
}
|
|
114
|
-
// The identity is the caller's claim about WHICH build was observed, and
|
|
115
|
-
// three documents called it exact while nothing read it. A map is the shape
|
|
116
|
-
// that carries it, so this is where it is refused.
|
|
117
|
-
if (!isExactVersion(surface.harnessVersion)) {
|
|
118
|
-
throw new Error(`surface.harnessVersion ${EXACT_VERSION_EXPECTED}; got ${JSON.stringify(surface.harnessVersion)}`);
|
|
119
|
-
}
|
|
120
|
-
const entries = policies.map((policy) => {
|
|
121
|
-
const result = probePolicy(policy, adapter, snapshot);
|
|
122
|
-
const entry = {
|
|
123
|
-
policyId: policy.policyId,
|
|
124
|
-
policyVersion: policy.policyVersion,
|
|
125
|
-
mechanism: policy.mechanism,
|
|
126
|
-
status: result.state,
|
|
127
|
-
verifiedAt: at,
|
|
128
|
-
verifiedBy: 'probe',
|
|
129
|
-
triggeredBy: trigger,
|
|
130
|
-
consecutiveMisses: 0,
|
|
131
|
-
};
|
|
132
|
-
if (result.reason !== undefined)
|
|
133
|
-
entry.degradationReason = result.reason;
|
|
134
|
-
if (evidencePointer !== undefined)
|
|
135
|
-
entry.evidencePointer = evidencePointer;
|
|
136
|
-
return entry;
|
|
137
|
-
});
|
|
138
|
-
return { surface, entries };
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Fold one observation of real traffic into an entry.
|
|
142
|
-
*
|
|
143
|
-
* `seen` is the whole input: the operation the policy judges happened, and the
|
|
144
|
-
* observable signal the mechanism should have produced either appeared or did
|
|
145
|
-
* not. There is no parameter for "and this much time has passed", because time
|
|
146
|
-
* is not evidence about a mechanism.
|
|
147
|
-
*/
|
|
148
|
-
export function observeExpectedSignal(entry, observation) {
|
|
149
|
-
const { seen, at, threshold = DEFAULT_DEGRADATION_THRESHOLD, reason } = observation;
|
|
150
|
-
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
151
|
-
throw new Error(`the degradation threshold must be a whole number of observations, at least 1; got ${String(threshold)}`);
|
|
152
|
-
}
|
|
153
|
-
requireTimestamp('at', at);
|
|
154
|
-
if (seen) {
|
|
155
|
-
const next = {
|
|
156
|
-
...entry,
|
|
157
|
-
consecutiveMisses: 0,
|
|
158
|
-
verifiedAt: at,
|
|
159
|
-
verifiedBy: 'traffic',
|
|
160
|
-
};
|
|
161
|
-
// A signal seen where the record says nothing is wired is not a promotion
|
|
162
|
-
// and not a miss: the wiring and the traffic disagree, and that is the
|
|
163
|
-
// state the item reserves for it. The status is not raised — see the
|
|
164
|
-
// second rule in this file's header — so the only status this branch
|
|
165
|
-
// produces is the one naming the contradiction.
|
|
166
|
-
if (entry.status === 'UNSUPPORTED') {
|
|
167
|
-
next.status = 'INTEGRATION-FAILED';
|
|
168
|
-
next.degradationReason = `the expected signal was observed although the recorded status was UNSUPPORTED, so the wiring and the traffic disagree`;
|
|
169
|
-
}
|
|
170
|
-
return next;
|
|
171
|
-
}
|
|
172
|
-
const consecutiveMisses = entry.consecutiveMisses + 1;
|
|
173
|
-
const degrades = entry.status === 'SUPPORTED' && consecutiveMisses >= threshold;
|
|
174
|
-
const next = {
|
|
175
|
-
...entry,
|
|
176
|
-
consecutiveMisses,
|
|
177
|
-
verifiedAt: at,
|
|
178
|
-
verifiedBy: 'traffic',
|
|
179
|
-
};
|
|
180
|
-
if (!degrades)
|
|
181
|
-
return next;
|
|
182
|
-
next.status = 'DEGRADED';
|
|
183
|
-
next.degradationReason =
|
|
184
|
-
reason ??
|
|
185
|
-
`the expected observable signal was absent on ${String(consecutiveMisses)} consecutive operations`;
|
|
186
|
-
return next;
|
|
187
|
-
}
|
|
188
|
-
/** The entry for one policy, or `null` — never an invented status. */
|
|
189
|
-
export function statusOf(map, policyId) {
|
|
190
|
-
return map.entries.find((entry) => entry.policyId === policyId) ?? null;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* The qualifier a verdict must carry when it was produced under this state.
|
|
194
|
-
*
|
|
195
|
-
* The unenforceable states mean the question could not be put to a working
|
|
196
|
-
* mechanism, so an `allow` under either is UNVERIFIABLE rather than a pass —
|
|
197
|
-
* `./decision-record.ts` refuses the unqualified record, and
|
|
198
|
-
* `packages/cli/test/policy-coverage.test.ts` › "refuses the silent pass an
|
|
199
|
-
* unwired surface would otherwise produce, and accepts it once qualifierFor
|
|
200
|
-
* speaks" holds the two modules to it together. How far that refusal reaches is
|
|
201
|
-
* stated once, at `UNENFORCEABLE_STATES` in `./vocabulary.ts` — read it there.
|
|
202
|
-
* This sentence used to paraphrase it as well, and the paraphrase is what went
|
|
203
|
-
* stale: it still said the refusal was "a property of records built the
|
|
204
|
-
* ordinary way" after RP-153 had made it hold against a hand-built prototype
|
|
205
|
-
* too. A second copy of a fact the same sentence claims is spelled once
|
|
206
|
-
* (`rules/invariants.md`, "One mechanism, one implementation").
|
|
207
|
-
*/
|
|
208
|
-
export function qualifierFor(state) {
|
|
209
|
-
return UNENFORCEABLE_STATES.includes(state)
|
|
210
|
-
? 'UNVERIFIABLE'
|
|
211
|
-
: undefined;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Every policy whose status fell between two maps of the same surface.
|
|
215
|
-
*
|
|
216
|
-
* Only the same policy id and declaration version present in both are compared:
|
|
217
|
-
* an entry that appeared, vanished or changed semantics is a change of what is
|
|
218
|
-
* being measured, not a capability that fell.
|
|
219
|
-
* Two maps of DIFFERENT surfaces are refused rather than diffed — the result
|
|
220
|
-
* is attributed to one surface identity, and attributing one surface's fall to
|
|
221
|
-
* another is a report an operator would act on in the wrong place.
|
|
222
|
-
*/
|
|
223
|
-
export function downgradesBetween(before, after) {
|
|
224
|
-
// Deliberately NOT harnessVersion. A probe before and after an upgrade
|
|
225
|
-
// carries two versions of one surface, and that pair is exactly what the
|
|
226
|
-
// `upgrade` trigger exists to compare — refusing it disabled the comparison
|
|
227
|
-
// for the case it was added for. Harness, surface and OS are what make it
|
|
228
|
-
// the same place.
|
|
229
|
-
const sameSurface = before.surface.harness === after.surface.harness &&
|
|
230
|
-
before.surface.surface === after.surface.surface &&
|
|
231
|
-
before.surface.os === after.surface.os;
|
|
232
|
-
if (!sameSurface) {
|
|
233
|
-
throw new Error('two coverage maps of different surfaces cannot be compared: a downgrade is attributed to one surface, and these name two');
|
|
234
|
-
}
|
|
235
|
-
const downgrades = [];
|
|
236
|
-
for (const next of after.entries) {
|
|
237
|
-
const previous = before.entries.find((entry) => entry.policyId === next.policyId && entry.policyVersion === next.policyVersion) ?? null;
|
|
238
|
-
if (previous === null)
|
|
239
|
-
continue;
|
|
240
|
-
if (ENFORCEMENT_RANK[next.status] <= ENFORCEMENT_RANK[previous.status])
|
|
241
|
-
continue;
|
|
242
|
-
downgrades.push({
|
|
243
|
-
surface: after.surface,
|
|
244
|
-
policyId: next.policyId,
|
|
245
|
-
from: previous.status,
|
|
246
|
-
to: next.status,
|
|
247
|
-
mechanism: next.mechanism,
|
|
248
|
-
reason: next.degradationReason ?? 'the entry recorded no reason',
|
|
249
|
-
at: next.verifiedAt,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
return downgrades;
|
|
253
|
-
}
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The decision-record schema (RP-76): what one verdict of one policy on one
|
|
3
|
-
* harness must carry to be audited later, and the validator that refuses a
|
|
4
|
-
* record which would read as more than it is.
|
|
5
|
-
*
|
|
6
|
-
* Emitting these at runtime is a separate task; this file is the shape and the
|
|
7
|
-
* rules. The rules that go beyond "the field is in its vocabulary":
|
|
8
|
-
*
|
|
9
|
-
* - the policy must be registered and the version compatible with it, so the
|
|
10
|
-
* record names semantics a reader can look up;
|
|
11
|
-
* - the operation must be one the policy declares;
|
|
12
|
-
* - a qualifier (`UNVERIFIABLE`, `UNMEASURED`) must carry a reason;
|
|
13
|
-
* - a capability state of `UNSUPPORTED` or `INTEGRATION-FAILED` must qualify
|
|
14
|
-
* the verdict `UNVERIFIABLE` — an unenforceable policy never yields a silent
|
|
15
|
-
* pass (`./vocabulary.ts`, `CAPABILITY_STATES`);
|
|
16
|
-
* - every evidence kind the policy requires must be present;
|
|
17
|
-
* - a policy that redacts must not be recorded with unredacted diagnostics;
|
|
18
|
-
* - the timestamp is supplied by the caller and must be an ISO-8601 date-time
|
|
19
|
-
* with seconds and an explicit zone (`ISO_8601` in `./validation.ts`, which
|
|
20
|
-
* every shape recording an observation time reads; a bare date is
|
|
21
|
-
* refused) — no clock here.
|
|
22
|
-
*
|
|
23
|
-
* Each rule is one test in `packages/cli/test/policy-declaration.test.ts`
|
|
24
|
-
* under "validating a decision record".
|
|
25
|
-
*
|
|
26
|
-
* 🔴 EVERY field of the input is read through `carriesField`/`ownField`
|
|
27
|
-
* (`./validation.ts`) — own and enumerable, the set `Object.keys` walks — and
|
|
28
|
-
* never off the record directly. Presence and value travel through the same
|
|
29
|
-
* predicate: `'qualifier' in value` was the load-bearing half of this defect,
|
|
30
|
-
* because `unknownKeys` judges the same record by `Object.keys` while the reads
|
|
31
|
-
* walked the prototype chain, so the closed-shape check and the field reads
|
|
32
|
-
* disagreed about what the record contained — with the reads being the wider of
|
|
33
|
-
* the two, which is the direction that passes. A decision record is the
|
|
34
|
-
* artifact a later reader audits, and the shape that made this worth fixing
|
|
35
|
-
* before it had a caller is a record whose verdict qualifier is only inherited:
|
|
36
|
-
* it validates as qualified, and what it then writes out is `{"outcome":
|
|
37
|
-
* "allow","reason":…}` — the reason survives, the QUALIFIER is what is lost,
|
|
38
|
-
* which is exactly the silent pass an `UNSUPPORTED` capability state exists to
|
|
39
|
-
* prevent. Held over every reading site in this module, in both shapes —
|
|
40
|
-
* inherited, and own but not enumerable — in
|
|
41
|
-
* `packages/cli/test/policy-declaration.test.ts` › "refuses an UNSUPPORTED
|
|
42
|
-
* record whose verdict qualifier is %s, because what it writes out is a silent
|
|
43
|
-
* pass", with the other direction held by › "still accepts a record whose every
|
|
44
|
-
* field is defined through Object.defineProperty as own and enumerable". The
|
|
45
|
-
* `%s` is the name as the `it.each` case DECLARES it; quoting one expanded case
|
|
46
|
-
* gives a pointer no grep lands on.
|
|
47
|
-
*
|
|
48
|
-
* 🔴 And an unnarrowed outside value reaches a MESSAGE only through `quote`
|
|
49
|
-
* (`./validation.ts`), never through bare `String` or `JSON.stringify`.
|
|
50
|
-
* Two qualifications, because an earlier version of this sentence claimed more
|
|
51
|
-
* than the file delivers. First, `operation` and `capabilityState` are still
|
|
52
|
-
* interpolated bare into messages, safely: `member` has narrowed each to its
|
|
53
|
-
* closed vocabulary before the message is built. Second — and this is the part
|
|
54
|
-
* the sentence used to hide — a `Problem`'s **`field`** is not a message and is
|
|
55
|
-
* not escaped: `unknownKeys` (`./validation.ts`) puts the outside key name
|
|
56
|
-
* there verbatim, and `./declaration.ts` › `definePolicy` renders a report as
|
|
57
|
-
* `${field}: ${message}`, so an unknown key whose NAME carries a newline still
|
|
58
|
-
* forges a line. Escaping a `field` would break it as the machine-readable
|
|
59
|
-
* pointer it is, so the fix belongs in the rendering; it is RP-160.
|
|
60
|
-
* Both unsafe spellings inside messages were here: a qualifier
|
|
61
|
-
* carrying a newline forged two `field:
|
|
62
|
-
* message` lines of its own in the rendered problem list — the exact shape
|
|
63
|
-
* `./declaration.ts` › `definePolicy` throws — while the neighbouring line
|
|
64
|
-
* escaped the same value; and a circular value crashed the validator with a
|
|
65
|
-
* `TypeError` where `quote` degrades. Held by ›
|
|
66
|
-
* "escapes a verdict qualifier carrying a newline, so it cannot forge a line of
|
|
67
|
-
* the refusal report" and › "refuses a record whose %s is a circular value,
|
|
68
|
-
* rather than throwing while it renders the refusal".
|
|
69
|
-
*
|
|
70
|
-
* ⚠ What this does NOT do, and the limits are stated rather than implied:
|
|
71
|
-
*
|
|
72
|
-
* - the `ok: true` value is the input object itself, not a snapshot of the
|
|
73
|
-
* fields that were certified. Those are the same thing for a value whose
|
|
74
|
-
* fields are plain data, which is every record `JSON.parse` can produce; they
|
|
75
|
-
* are not the same for one carrying a live accessor, which validates on one
|
|
76
|
-
* read and serialises from another. RP-157 owns that, for this module and
|
|
77
|
-
* `./declaration.ts` together;
|
|
78
|
-
* - nothing here caps the cost, and the amplifier is the PROBLEM LIST rather
|
|
79
|
-
* than the input: one `Problem` is pushed per bad entry of `observedFacts` or
|
|
80
|
-
* `evidence`, so a parsed record carrying a million bad entries produces a
|
|
81
|
-
* million objects. That is reachable from ordinary `JSON.parse` input. The
|
|
82
|
-
* first version of this bullet was wrong in every clause — it said
|
|
83
|
-
* `Array.prototype.forEach` VISITS a sparse array's holes, that
|
|
84
|
-
* `new Array(1e7)` cost seconds and gigabytes "before the refusal", and that
|
|
85
|
-
* the cost was unreachable from a parsed record. `forEach` SKIPS holes, a
|
|
86
|
-
* sparse `observedFacts` is not refused at all, and the dense case is the
|
|
87
|
-
* reachable one. Corrected rather than softened, because a stale limit sells
|
|
88
|
-
* cover that is not there (`rules/invariants.md`, "State the limits — and
|
|
89
|
-
* test them");
|
|
90
|
-
* - an array HOLE is consequently read by nothing, while `JSON.stringify`
|
|
91
|
-
* writes it out as `null`: `observedFacts: [{…}, , ,]` serialises as
|
|
92
|
-
* `[{…},null,null]` and validates. That is this module's own rule — read what
|
|
93
|
-
* the serialisation carries — failing one level BELOW the field, which is
|
|
94
|
-
* where this change did not look. RP-161;
|
|
95
|
-
* - `quote` itself re-throws for a value whose `JSON.stringify` and `String`
|
|
96
|
-
* both throw. The direction is a crash, never an `ok: true`, so nothing
|
|
97
|
-
* malformed is certified through it — RP-160.
|
|
98
|
-
*
|
|
99
|
-
* ⚠ `diagnostics.redacted` is the emitter's claim, and this validator enforces
|
|
100
|
-
* the claim's presence, not the property: a record marked redacted whose
|
|
101
|
-
* `diagnostics.text`, `observedFacts[].value` or `evidence[].value` still
|
|
102
|
-
* carries a matched value is accepted here. Scanning content would pull the
|
|
103
|
-
* secret vocabulary into the core, which the dependency-direction test
|
|
104
|
-
* forbids — so the emitting task owns that scan, over those three fields,
|
|
105
|
-
* before it persists a record.
|
|
106
|
-
*/
|
|
107
|
-
import { compatibilityOf, findPolicy } from './registry.js';
|
|
108
|
-
import { CAPABILITY_STATES, UNENFORCEABLE_STATES, DECISION_OUTCOMES, EVIDENCE_KINDS, OPERATIONS, VERDICT_QUALIFIERS, } from './vocabulary.js';
|
|
109
|
-
import { ISO_8601, carriesField, isRecord, member, nonEmptyString, ownField, quote, unknownKeys, } from './validation.js';
|
|
110
|
-
export const DECISION_RECORD_SCHEMA_VERSION = 1;
|
|
111
|
-
const KEYS = [
|
|
112
|
-
'schemaVersion',
|
|
113
|
-
'policyId',
|
|
114
|
-
'policyVersion',
|
|
115
|
-
'harness',
|
|
116
|
-
'operation',
|
|
117
|
-
'capabilityState',
|
|
118
|
-
'observedFacts',
|
|
119
|
-
'verdict',
|
|
120
|
-
'evidence',
|
|
121
|
-
'artifactVersion',
|
|
122
|
-
'diagnostics',
|
|
123
|
-
'recordedAt',
|
|
124
|
-
];
|
|
125
|
-
/**
|
|
126
|
-
* Read from `./vocabulary.ts` rather than restated here: `./coverage.ts` ›
|
|
127
|
-
* `qualifierFor` answers from the same list, and a second copy is how the two
|
|
128
|
-
* come to disagree about which verdicts may pass unqualified.
|
|
129
|
-
*/
|
|
130
|
-
const NEVER_SILENT_PASS = UNENFORCEABLE_STATES;
|
|
131
|
-
const namedPairs = (problems, field, value, keys) => {
|
|
132
|
-
if (!Array.isArray(value)) {
|
|
133
|
-
problems.push({ field, message: 'must be a list' });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
value.forEach((entry, index) => {
|
|
137
|
-
if (!isRecord(entry)) {
|
|
138
|
-
problems.push({ field: `${field}[${index}]`, message: 'must be an object' });
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
unknownKeys(problems, entry, keys, `${field}[${index}]`);
|
|
142
|
-
for (const key of keys)
|
|
143
|
-
nonEmptyString(problems, `${field}[${index}].${key}`, ownField(entry, key));
|
|
144
|
-
});
|
|
145
|
-
};
|
|
146
|
-
const checkVerdict = (problems, value, capabilityState) => {
|
|
147
|
-
if (!isRecord(value)) {
|
|
148
|
-
problems.push({ field: 'verdict', message: 'must be an object' });
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
unknownKeys(problems, value, ['outcome', 'qualifier', 'reason'], 'verdict');
|
|
152
|
-
member(problems, 'verdict.outcome', ownField(value, 'outcome'), DECISION_OUTCOMES);
|
|
153
|
-
const qualifier = ownField(value, 'qualifier');
|
|
154
|
-
const reason = ownField(value, 'reason');
|
|
155
|
-
if (carriesField(value, 'qualifier')) {
|
|
156
|
-
member(problems, 'verdict.qualifier', qualifier, VERDICT_QUALIFIERS);
|
|
157
|
-
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
158
|
-
problems.push({
|
|
159
|
-
field: 'verdict.reason',
|
|
160
|
-
message: `a ${quote(qualifier)} verdict must say why`,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
else if (carriesField(value, 'reason') && typeof reason !== 'string') {
|
|
165
|
-
problems.push({ field: 'verdict.reason', message: 'must be a string when present' });
|
|
166
|
-
}
|
|
167
|
-
if (capabilityState !== null && NEVER_SILENT_PASS.includes(capabilityState)) {
|
|
168
|
-
if (qualifier !== 'UNVERIFIABLE') {
|
|
169
|
-
problems.push({
|
|
170
|
-
field: 'verdict.qualifier',
|
|
171
|
-
message: `capabilityState ${capabilityState} never yields a silent pass: ` +
|
|
172
|
-
'the verdict must be qualified UNVERIFIABLE with a reason',
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
const checkEvidence = (problems, value, required) => {
|
|
178
|
-
if (!Array.isArray(value)) {
|
|
179
|
-
problems.push({ field: 'evidence', message: 'must be a list' });
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const present = new Set();
|
|
183
|
-
value.forEach((entry, index) => {
|
|
184
|
-
if (!isRecord(entry)) {
|
|
185
|
-
problems.push({ field: `evidence[${index}]`, message: 'must be an object' });
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
unknownKeys(problems, entry, ['kind', 'value'], `evidence[${index}]`);
|
|
189
|
-
const kind = ownField(entry, 'kind');
|
|
190
|
-
if (member(problems, `evidence[${index}].kind`, kind, EVIDENCE_KINDS)) {
|
|
191
|
-
present.add(kind);
|
|
192
|
-
}
|
|
193
|
-
nonEmptyString(problems, `evidence[${index}].value`, ownField(entry, 'value'));
|
|
194
|
-
});
|
|
195
|
-
for (const kind of required) {
|
|
196
|
-
if (!present.has(kind)) {
|
|
197
|
-
problems.push({ field: 'evidence', message: `the policy requires ${kind} evidence` });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
const checkDiagnostics = (problems, value, mustRedact) => {
|
|
202
|
-
if (!isRecord(value)) {
|
|
203
|
-
problems.push({ field: 'diagnostics', message: 'must be an object' });
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
unknownKeys(problems, value, ['redacted', 'text'], 'diagnostics');
|
|
207
|
-
const redacted = ownField(value, 'redacted');
|
|
208
|
-
if (typeof redacted !== 'boolean') {
|
|
209
|
-
problems.push({ field: 'diagnostics.redacted', message: 'must be a boolean' });
|
|
210
|
-
}
|
|
211
|
-
else if (mustRedact && !redacted) {
|
|
212
|
-
problems.push({
|
|
213
|
-
field: 'diagnostics.redacted',
|
|
214
|
-
message: 'the policy redacts matched values, so its diagnostics must be recorded redacted',
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
if (typeof ownField(value, 'text') !== 'string') {
|
|
218
|
-
problems.push({ field: 'diagnostics.text', message: 'must be a string' });
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
/** Validate an unknown value as a decision record, reporting every problem at once. */
|
|
222
|
-
export function validateDecisionRecord(input) {
|
|
223
|
-
if (!isRecord(input)) {
|
|
224
|
-
return { ok: false, problems: [{ field: '', message: 'a decision record is an object' }] };
|
|
225
|
-
}
|
|
226
|
-
const problems = [];
|
|
227
|
-
unknownKeys(problems, input, KEYS);
|
|
228
|
-
const schemaVersion = ownField(input, 'schemaVersion');
|
|
229
|
-
if (schemaVersion !== DECISION_RECORD_SCHEMA_VERSION) {
|
|
230
|
-
problems.push({
|
|
231
|
-
field: 'schemaVersion',
|
|
232
|
-
message: `must be ${DECISION_RECORD_SCHEMA_VERSION}, got ${quote(schemaVersion)}`,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
const policyId = ownField(input, 'policyId');
|
|
236
|
-
const policyVersion = ownField(input, 'policyVersion');
|
|
237
|
-
const policy = typeof policyId === 'string' ? findPolicy(policyId) : null;
|
|
238
|
-
if (policy === null) {
|
|
239
|
-
problems.push({
|
|
240
|
-
field: 'policyId',
|
|
241
|
-
message: `${quote(policyId)} is not a registered policy`,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
else if (typeof policyVersion === 'string') {
|
|
245
|
-
const compatibility = compatibilityOf(policy.policyId, policyVersion);
|
|
246
|
-
if (compatibility !== 'compatible') {
|
|
247
|
-
problems.push({
|
|
248
|
-
field: 'policyVersion',
|
|
249
|
-
message: `${quote(policyVersion)} is ${compatibility} with ${policy.policyId} ${policy.policyVersion}`,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
nonEmptyString(problems, 'policyVersion', policyVersion);
|
|
255
|
-
}
|
|
256
|
-
nonEmptyString(problems, 'harness', ownField(input, 'harness'));
|
|
257
|
-
const operation = ownField(input, 'operation');
|
|
258
|
-
if (member(problems, 'operation', operation, OPERATIONS) && policy !== null) {
|
|
259
|
-
if (!policy.operations.includes(operation)) {
|
|
260
|
-
problems.push({
|
|
261
|
-
field: 'operation',
|
|
262
|
-
message: `${policy.policyId} does not apply to ${operation}`,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const state = ownField(input, 'capabilityState');
|
|
267
|
-
const capabilityState = member(problems, 'capabilityState', state, CAPABILITY_STATES)
|
|
268
|
-
? state
|
|
269
|
-
: null;
|
|
270
|
-
namedPairs(problems, 'observedFacts', ownField(input, 'observedFacts'), ['name', 'value']);
|
|
271
|
-
checkVerdict(problems, ownField(input, 'verdict'), capabilityState);
|
|
272
|
-
checkEvidence(problems, ownField(input, 'evidence'), policy?.requiredEvidence ?? []);
|
|
273
|
-
nonEmptyString(problems, 'artifactVersion', ownField(input, 'artifactVersion'));
|
|
274
|
-
checkDiagnostics(problems, ownField(input, 'diagnostics'), policy !== null && policy.redaction !== 'none');
|
|
275
|
-
const recordedAt = ownField(input, 'recordedAt');
|
|
276
|
-
if (typeof recordedAt !== 'string' ||
|
|
277
|
-
!ISO_8601.test(recordedAt) ||
|
|
278
|
-
Number.isNaN(Date.parse(recordedAt))) {
|
|
279
|
-
problems.push({
|
|
280
|
-
field: 'recordedAt',
|
|
281
|
-
message: `must be an ISO-8601 date-time with seconds and an explicit zone, got ${quote(recordedAt)}`,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
if (problems.length > 0)
|
|
285
|
-
return { ok: false, problems };
|
|
286
|
-
return { ok: true, value: input };
|
|
287
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The typed policy declaration (RP-76).
|
|
3
|
-
*
|
|
4
|
-
* A declaration is the semantic source of one policy: what the invariant is,
|
|
5
|
-
* where it applies, what the mechanism can answer, how it fails, what a
|
|
6
|
-
* verdict must carry as evidence, and which version of those semantics a
|
|
7
|
-
* decision record is talking about. It is data plus a validator — not a policy
|
|
8
|
-
* language, not an interpreter, not a compiler: a harness adapter reads a
|
|
9
|
-
* declaration and names its own native surface for it (`./adapter.ts`), and
|
|
10
|
-
* that is the whole of the transformation.
|
|
11
|
-
*
|
|
12
|
-
* The shape is closed: a field this interface does not name is refused, so a
|
|
13
|
-
* field added by mistake cannot travel unnoticed into a record somebody later
|
|
14
|
-
* audits. Every enumerated field draws from `./vocabulary.ts`.
|
|
15
|
-
*
|
|
16
|
-
* 🔴 `validateDeclaration` reads every field of its input through `ownField`
|
|
17
|
-
* (`./validation.ts`) — own and enumerable, the set `Object.keys` walks — and
|
|
18
|
-
* never off the record directly. `unknownKeys` on the line above already judges
|
|
19
|
-
* the record by `Object.keys`, so a wider read made the closed-shape check and
|
|
20
|
-
* the field reads disagree about what the record contains, with the reads being
|
|
21
|
-
* the wider of the two — the direction that passes. The consequence here is one
|
|
22
|
-
* step worse than in `./decision-record.ts`, because this validator's answer is
|
|
23
|
-
* kept: `definePolicy` copies the validated declaration by spread, and a spread
|
|
24
|
-
* copies own-enumerable keys only. So a declaration whose `tier` and `redaction`
|
|
25
|
-
* were only inherited used to be accepted and then FROZEN into the registry
|
|
26
|
-
* with no `tier` key and no `redaction` key at all — reading either yields
|
|
27
|
-
* `undefined` — a malformed entry produced by the function whose whole job is to
|
|
28
|
-
* refuse malformed ones. Held over all fifteen fields, in both shapes (inherited,
|
|
29
|
-
* and own but not enumerable), in `packages/cli/test/policy-declaration.test.ts`
|
|
30
|
-
* › "refuses a declaration whose %s is %s, because the declaration it writes out
|
|
31
|
-
* carries no such field" and, for the frozen-entry consequence, › "refuses a
|
|
32
|
-
* declaration whose tier is %s, rather than freezing an entry with no tier at
|
|
33
|
-
* all". The `%s` are the names as the `it.each` cases DECLARE them — quoting an
|
|
34
|
-
* expanded case instead is a pointer no grep lands on, which is what
|
|
35
|
-
* `test/template/evidence-pointers.test.ts` treats as a wildcard and what
|
|
36
|
-
* `rules/invariants.md` means by "the test's whole name".
|
|
37
|
-
*
|
|
38
|
-
* ⚠ What this does NOT do: `validateDeclaration` returns the input object
|
|
39
|
-
* itself (`input as unknown as PolicyDeclaration`), so what a caller gets back
|
|
40
|
-
* is the record it passed in, not a snapshot of the fields that were certified.
|
|
41
|
-
* Those are the same thing for a value whose fields are plain DATA, which is
|
|
42
|
-
* every declaration `JSON.parse` can produce; they are not the same for one
|
|
43
|
-
* carrying a live accessor. **An object literal is not the line** — a literal
|
|
44
|
-
* can carry a getter, and an earlier version of this sentence put literals on
|
|
45
|
-
* the safe side, which named the one shape that reaches the gap as the shape
|
|
46
|
-
* that avoids it. Here the consequence is kept rather than merely returned:
|
|
47
|
-
* `definePolicy` spreads the validated value, so a getter validated on read 1
|
|
48
|
-
* is FROZEN into the registry from read 2. That gap is RP-157, and
|
|
49
|
-
* `./decision-record.ts` states the identical limit at its own return.
|
|
50
|
-
*/
|
|
51
|
-
import { AUTONOMY_TIERS, DECISION_OUTCOMES, ENFORCEMENT_TIMINGS, EVIDENCE_KINDS, FAILURE_SEMANTICS, HARNESS_CAPABILITIES, LIFECYCLE_STATES, OPERATIONS, REDACTION_RULES, } from './vocabulary.js';
|
|
52
|
-
import { isRecord, matching, member, members, nonEmptyString, ownField, unknownKeys, } from './validation.js';
|
|
53
|
-
/**
|
|
54
|
-
* The closed set of fields a declaration may carry — exported so a test can
|
|
55
|
-
* check the fixture against THIS list by name rather than by counting it.
|
|
56
|
-
* A count agrees with a set that has drifted; `rules/invariants.md`, "One
|
|
57
|
-
* mechanism, one implementation".
|
|
58
|
-
*/
|
|
59
|
-
export const KEYS = [
|
|
60
|
-
'policyId',
|
|
61
|
-
'policyVersion',
|
|
62
|
-
'lifecycle',
|
|
63
|
-
'invariant',
|
|
64
|
-
'tier',
|
|
65
|
-
'operations',
|
|
66
|
-
'timing',
|
|
67
|
-
'requiredCapability',
|
|
68
|
-
'mechanism',
|
|
69
|
-
'outcomes',
|
|
70
|
-
'onInternalError',
|
|
71
|
-
'onUnreadableInput',
|
|
72
|
-
'requiredEvidence',
|
|
73
|
-
'redaction',
|
|
74
|
-
'statedIn',
|
|
75
|
-
];
|
|
76
|
-
export const KEBAB_CASE = /^[a-z][a-z0-9-]*$/;
|
|
77
|
-
export const POLICY_VERSION = /^\d+\.\d+$/;
|
|
78
|
-
/** Validate an unknown value as a declaration, reporting every problem at once. */
|
|
79
|
-
export function validateDeclaration(input) {
|
|
80
|
-
if (!isRecord(input)) {
|
|
81
|
-
return { ok: false, problems: [{ field: '', message: 'a declaration is an object' }] };
|
|
82
|
-
}
|
|
83
|
-
const problems = [];
|
|
84
|
-
unknownKeys(problems, input, KEYS);
|
|
85
|
-
matching(problems, 'policyId', ownField(input, 'policyId'), KEBAB_CASE, 'kebab-case');
|
|
86
|
-
matching(problems, 'policyVersion', ownField(input, 'policyVersion'), POLICY_VERSION, 'MAJOR.MINOR');
|
|
87
|
-
member(problems, 'lifecycle', ownField(input, 'lifecycle'), LIFECYCLE_STATES);
|
|
88
|
-
nonEmptyString(problems, 'invariant', ownField(input, 'invariant'));
|
|
89
|
-
member(problems, 'tier', ownField(input, 'tier'), AUTONOMY_TIERS);
|
|
90
|
-
members(problems, 'operations', ownField(input, 'operations'), OPERATIONS, { nonEmpty: true });
|
|
91
|
-
member(problems, 'timing', ownField(input, 'timing'), ENFORCEMENT_TIMINGS);
|
|
92
|
-
member(problems, 'requiredCapability', ownField(input, 'requiredCapability'), HARNESS_CAPABILITIES);
|
|
93
|
-
matching(problems, 'mechanism', ownField(input, 'mechanism'), KEBAB_CASE, 'kebab-case');
|
|
94
|
-
members(problems, 'outcomes', ownField(input, 'outcomes'), DECISION_OUTCOMES, { nonEmpty: true });
|
|
95
|
-
member(problems, 'onInternalError', ownField(input, 'onInternalError'), FAILURE_SEMANTICS);
|
|
96
|
-
member(problems, 'onUnreadableInput', ownField(input, 'onUnreadableInput'), FAILURE_SEMANTICS);
|
|
97
|
-
members(problems, 'requiredEvidence', ownField(input, 'requiredEvidence'), EVIDENCE_KINDS, {
|
|
98
|
-
nonEmpty: false,
|
|
99
|
-
});
|
|
100
|
-
member(problems, 'redaction', ownField(input, 'redaction'), REDACTION_RULES);
|
|
101
|
-
nonEmptyString(problems, 'statedIn', ownField(input, 'statedIn'));
|
|
102
|
-
if (problems.length > 0)
|
|
103
|
-
return { ok: false, problems };
|
|
104
|
-
// Every field above was checked against the shape, so the narrowing is earned
|
|
105
|
-
// rather than asserted: the cast is to the type the checks just established.
|
|
106
|
-
return { ok: true, value: input };
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Define a policy: validate it and hand back a frozen copy. Throws an Error
|
|
110
|
-
* whose message lists every problem as `field: message`, so a registry that
|
|
111
|
-
* loads at import time fails with the whole list rather than one line at a
|
|
112
|
-
* time.
|
|
113
|
-
*/
|
|
114
|
-
export function definePolicy(input) {
|
|
115
|
-
const result = validateDeclaration(input);
|
|
116
|
-
if (!result.ok) {
|
|
117
|
-
const lines = result.problems.map(({ field, message }) => `${field}: ${message}`);
|
|
118
|
-
throw new Error(`invalid policy declaration:\n ${lines.join('\n ')}`);
|
|
119
|
-
}
|
|
120
|
-
const policy = result.value;
|
|
121
|
-
return Object.freeze({
|
|
122
|
-
...policy,
|
|
123
|
-
operations: Object.freeze([...policy.operations]),
|
|
124
|
-
outcomes: Object.freeze([...policy.outcomes]),
|
|
125
|
-
requiredEvidence: Object.freeze([...policy.requiredEvidence]),
|
|
126
|
-
});
|
|
127
|
-
}
|