@pareto4/pp-core 1.13.0 → 1.17.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/.claude-plugin/plugin.json +3 -3
- package/README.md +26 -9
- package/agents/pp-code-reviewer.md +2 -1
- package/agents/pp-debug-session-manager.md +9 -6
- package/agents/pp-executor.md +10 -13
- package/agents/pp-plan-checker.md +4 -4
- package/agents/pp-planner.md +23 -9
- package/agents/pp-vault-reviewer.md +244 -21
- package/agents/pp-verifier.md +7 -3
- package/bin/install.js +10 -3
- package/commands/pp-execute-phase.md +1 -3
- package/commands/pp-ns-review.md +1 -1
- package/commands/pp-plan-phase.md +1 -2
- package/commands/pp-vault-review.md +5 -3
- package/commands/pp-verify-work.md +1 -2
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update.js +0 -0
- package/hooks/dist/pp-cursor-session-start.js +1 -1
- package/hooks/dist/pp-statusline.js +1 -1
- package/hooks/dist/pp-update-banner.js +0 -0
- package/hooks/pp-check-update.js +0 -0
- package/hooks/pp-cursor-session-start.js +1 -1
- package/hooks/pp-statusline.js +1 -1
- package/hooks/pp-update-banner.js +0 -0
- package/package.json +14 -19
- package/pareto-skills/_shared/create-project-board.sh +0 -0
- package/pareto-skills/pp-project-setup/SKILL.md +1 -1
- package/pp-core/bin/check-latest-version.cjs +0 -0
- package/pp-core/bin/detect-build-commands.sh +2 -2
- package/pp-core/bin/lib/capability-activation.cjs +83 -9
- package/pp-core/bin/lib/capability-registry.cjs +396 -82
- package/pp-core/bin/lib/capability-state.cjs +160 -4
- package/pp-core/bin/lib/capability-writer.cjs +19 -5
- package/pp-core/bin/lib/check-command-router.cjs +3334 -946
- package/pp-core/bin/lib/clusters.cjs +3 -0
- package/pp-core/bin/lib/command-aliases.cjs +0 -35
- package/pp-core/bin/lib/commands.cjs +9 -4
- package/pp-core/bin/lib/compose-bindings.cjs +251 -88
- package/pp-core/bin/lib/config-loader.cjs +7 -103
- package/pp-core/bin/lib/config-schema.cjs +1 -1
- package/pp-core/bin/lib/config.cjs +154 -62
- package/pp-core/bin/lib/decisions.cjs +4 -4
- package/pp-core/bin/lib/discovery-vault-check.cjs +246 -0
- package/pp-core/bin/lib/frontmatter.cjs +1 -38
- package/pp-core/bin/lib/git-base-branch.cjs +13 -11
- package/pp-core/bin/lib/git-provider.cjs +4 -4
- package/pp-core/bin/lib/init-command-router.cjs +5 -8
- package/pp-core/bin/lib/init-contract.cjs +648 -738
- package/pp-core/bin/lib/init-interactive.cjs +76 -4
- package/pp-core/bin/lib/init.cjs +24 -51
- package/pp-core/bin/lib/install-profiles.cjs +1 -1
- package/pp-core/bin/lib/io.cjs +8 -0
- package/pp-core/bin/lib/loop-resolver.cjs +189 -129
- package/pp-core/bin/lib/org-profile.cjs +469 -152
- package/pp-core/bin/lib/package-identity.cjs +3 -3
- package/pp-core/bin/lib/phase-command-router.cjs +1 -29
- package/pp-core/bin/lib/phase-scope.cjs +1 -1
- package/pp-core/bin/lib/phase.cjs +11 -253
- package/pp-core/bin/lib/platform-bindings.cjs +40 -19
- package/pp-core/bin/lib/practices-review-check.cjs +15 -9
- package/pp-core/bin/lib/profile-output.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +104 -1
- package/pp-core/bin/lib/state.cjs +11 -85
- package/pp-core/bin/lib/template.cjs +1 -1
- package/pp-core/bin/lib/vault-audit.cjs +650 -126
- package/pp-core/bin/lib/vault-command-router.cjs +2604 -367
- package/pp-core/bin/lib/vault-coordinates.cjs +13 -4
- package/pp-core/bin/lib/vault-index.cjs +699 -0
- package/pp-core/bin/lib/vault-mode.cjs +235 -0
- package/pp-core/bin/lib/vault-practices.cjs +139 -72
- package/pp-core/bin/lib/vault-review.cjs +194 -451
- package/pp-core/bin/lib/vault-seal.cjs +1936 -0
- package/pp-core/bin/lib/vault-standards.cjs +1505 -0
- package/pp-core/bin/lib/vault-trace.cjs +607 -254
- package/pp-core/bin/lib/vault-update.cjs +9 -33
- package/pp-core/bin/lib/vault-write-back.cjs +70 -42
- package/pp-core/bin/lib/vault.cjs +2734 -920
- package/pp-core/bin/lib/verify.cjs +0 -11
- package/pp-core/bin/portable-timeout.sh +1 -1
- package/pp-core/bin/pp-tools.cjs +184 -54
- package/pp-core/bin/shared/config-defaults.manifest.json +0 -7
- package/pp-core/bin/shared/config-schema.manifest.json +0 -3
- package/pp-core/bin/shared/model-catalog.json +2 -2
- package/pp-core/bin/verify-reapply-patches.cjs +0 -0
- package/pp-core/org-profile.json +26 -0
- package/pp-core/references/autonomous-best-effort.md +8 -7
- package/pp-core/references/loop-hook-dispatch.md +0 -25
- package/pp-core/references/planner-gap-closure.md +0 -1
- package/pp-core/references/planner-tdd.md +0 -1
- package/pp-core/references/planner-vault-practices.md +17 -34
- package/pp-core/references/planner-vault-standards.md +320 -0
- package/pp-core/references/planning-config.md +0 -8
- package/pp-core/references/practice-arithmetic-contract.md +121 -0
- package/pp-core/references/tdd.md +0 -1
- package/pp-core/references/verifier-practice-verdicts.md +263 -0
- package/pp-core/templates/README.md +1 -0
- package/pp-core/templates/VAULT-REVIEW.md +202 -0
- package/pp-core/templates/phase-prompt.md +2 -6
- package/pp-core/templates/practices/README.md +20 -23
- package/pp-core/workflows/_runtime-launcher.snippet.sh +0 -0
- package/pp-core/workflows/add-tests.md +55 -4
- package/pp-core/workflows/audit-fix.md +63 -2
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/autonomous.md +85 -11
- package/pp-core/workflows/check-todos.md +13 -2
- package/pp-core/workflows/code-review-fix.md +59 -3
- package/pp-core/workflows/code-review.md +10 -1
- package/pp-core/workflows/complete-milestone.md +1 -1
- package/pp-core/workflows/debug.md +54 -4
- package/pp-core/workflows/discovery-sprint/prefill.md +19 -7
- package/pp-core/workflows/discovery-sprint/prototype.md +46 -49
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +20 -24
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +1 -6
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +1 -3
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -3
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +213 -0
- package/pp-core/workflows/discovery-sprint.md +7 -3
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +4 -4
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +43 -1
- package/pp-core/workflows/discovery-to-build.md +10 -2
- package/pp-core/workflows/discuss-phase.md +3 -10
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +329 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +3 -2
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -1
- package/pp-core/workflows/execute-phase/steps/execute-pre-dispatch.md +98 -0
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +94 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +6 -8
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +115 -14
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +322 -20
- package/pp-core/workflows/execute-phase/steps/verifier-practice-payload.md +165 -0
- package/pp-core/workflows/execute-phase/steps/verify-post-gate.md +163 -0
- package/pp-core/workflows/execute-phase.md +155 -128
- package/pp-core/workflows/execute-plan.md +56 -6
- package/pp-core/workflows/fast.md +53 -4
- package/pp-core/workflows/help/modes/full.md +3 -3
- package/pp-core/workflows/init/steps/client-context-capture.md +101 -0
- package/pp-core/workflows/init/steps/gitlab-provisioning.md +130 -0
- package/pp-core/workflows/init/steps/project-md-deferral-narration.md +66 -0
- package/pp-core/workflows/init/steps/reachability-outcome.md +70 -0
- package/pp-core/workflows/init.md +232 -272
- package/pp-core/workflows/new-project.md +31 -23
- package/pp-core/workflows/plan-phase.md +98 -45
- package/pp-core/workflows/quick.md +117 -3
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/ship.md +57 -28
- package/pp-core/workflows/undo.md +1 -1
- package/pp-core/workflows/update.md +3 -3
- package/pp-core/workflows/validate-phase.md +58 -3
- package/pp-core/workflows/vault-review.md +282 -22
- package/pp-core/workflows/verify-work.md +12 -14
- package/scripts/changeset/cli.cjs +6 -5
- package/scripts/changeset/lint.cjs +0 -0
- package/scripts/changeset/new.cjs +33 -50
- package/scripts/dev-env.cjs +25 -85
- package/scripts/gen-capability-registry.cjs +199 -11
- package/scripts/gen-practice-arithmetic-contract.cjs +412 -0
- package/scripts/lib/allowlist-ratchet.cjs +1 -103
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
- package/scripts/lint-docs-required.cjs +0 -0
- package/scripts/lint-pr-check-project-dir.cjs +1 -3
- package/scripts/lint-test-file-count.allowlist.json +12 -10
- package/scripts/practice-arithmetic-contract.allow.json +35 -0
- package/scripts/prompt-injection-scan.sh +22 -17
- package/scripts/record-vault-mode-golden.cjs +89 -0
- package/scripts/release-channel.cjs +0 -0
- package/scripts/sync-manifest-versions.cjs +5 -4
- package/scripts/sync-rulesets.sh +1 -1
- package/scripts/update-size-baseline.cjs +30 -274
- package/scripts/vault-calibrate.cjs +996 -0
- package/scripts/verify-opengsd-sweep.cjs +3 -5
- package/scripts/workflow-size.cjs +9 -61
- package/pp-core/bin/lib/dispatch-plan.cjs +0 -483
- package/pp-core/bin/lib/install-run.cjs +0 -536
- package/pp-core/bin/lib/loop-gates.cjs +0 -213
- package/pp-core/bin/lib/phase-manifest.cjs +0 -527
- package/pp-core/bin/lib/phase-tier.cjs +0 -480
- package/pp-core/bin/lib/plan-graph.cjs +0 -368
- package/pp-core/references/executor-vault-trace.md +0 -47
- package/pp-core/references/planner-plan-graph.md +0 -66
- package/pp-core/workflows/discovery-sprint/vault-fetch.md +0 -37
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +0 -45
- package/pp-core/workflows/execute-phase/steps/install-run.md +0 -29
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +0 -23
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +0 -73
- package/pp-core/workflows/execute-phase/steps/vault-door-gate.md +0 -19
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +0 -43
- package/scripts/lib/size-budget.cjs +0 -311
- package/scripts/lint-neutrality-names.cjs +0 -694
- package/scripts/lint-org-coordinates.allowlist.json +0 -35
- package/scripts/lint-org-coordinates.cjs +0 -426
|
@@ -568,6 +568,18 @@ function validateFeatureBody(cap) {
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
// Quick 261015-ect (QD-13): requiresPlatformBindings, optional. When present it
|
|
572
|
+
// must be a boolean; the loop resolver drops every hook of a capability that
|
|
573
|
+
// declares `true` on a project with no .platform-bindings.json. Which
|
|
574
|
+
// capabilities MUST declare it is a cross-capability rule
|
|
575
|
+
// (validatePlatformBindingsField).
|
|
576
|
+
if (cap.requiresPlatformBindings !== undefined && typeof cap.requiresPlatformBindings !== 'boolean') {
|
|
577
|
+
errors.push(
|
|
578
|
+
'capability "' + (cap.id || '(unknown)') + '" requiresPlatformBindings must be a boolean (got: ' +
|
|
579
|
+
JSON.stringify(cap.requiresPlatformBindings) + ')',
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
571
583
|
return errors;
|
|
572
584
|
}
|
|
573
585
|
|
|
@@ -1204,17 +1216,6 @@ function validateGate(gate, prefix) {
|
|
|
1204
1216
|
if (count !== 1) {
|
|
1205
1217
|
errors.push(prefix + '.check must have exactly one of: query, predicate, agentVerdict');
|
|
1206
1218
|
}
|
|
1207
|
-
// check.args: literal tokens `loop run-gates` appends after the phase — query gates only,
|
|
1208
|
-
// at most 8, each a flag or a plain word (the runner refuses anything else at run time too).
|
|
1209
|
-
if (Object.prototype.hasOwnProperty.call(gate.check, 'args')) {
|
|
1210
|
-
const a = gate.check.args;
|
|
1211
|
-
if (!hasQuery) {
|
|
1212
|
-
errors.push(prefix + '.check.args is only valid beside check.query');
|
|
1213
|
-
} else if (!Array.isArray(a) || a.length > 8
|
|
1214
|
-
|| !a.every((t) => typeof t === 'string' && /^(?:--[a-z][a-z0-9-]*|[a-z0-9][a-z0-9._-]*)$/i.test(t))) {
|
|
1215
|
-
errors.push(prefix + '.check.args must be an array of at most 8 flags (--name) or plain words');
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
1219
|
// agentVerdict forces blocking: false (advisory only)
|
|
1219
1220
|
if (hasAgentVerdict && gate.blocking === true) {
|
|
1220
1221
|
errors.push(
|
|
@@ -1330,6 +1331,177 @@ function validateAgainstContract(cap, capId, knownAgentIds) {
|
|
|
1330
1331
|
return errors;
|
|
1331
1332
|
}
|
|
1332
1333
|
|
|
1334
|
+
/**
|
|
1335
|
+
* Phase 15 D-02: validate the per-hook `vaultMode` filter field.
|
|
1336
|
+
*
|
|
1337
|
+
* The pure loop resolver evaluates `vaultMode` against an injected mode value
|
|
1338
|
+
* and never emits it, so an unknown or misplaced value must be refused HERE,
|
|
1339
|
+
* at generation, never interpreted at runtime. Three rules, each reported on
|
|
1340
|
+
* its own:
|
|
1341
|
+
* (a) the field is allowed on contributions and gates only (a step is
|
|
1342
|
+
* refused);
|
|
1343
|
+
* (b) the value must be a string in the vault capability's declared
|
|
1344
|
+
* `config["vault.mode"].values`. That enum declaration is the ONE owner of
|
|
1345
|
+
* the closed set; it is read here, never restated. With no vault
|
|
1346
|
+
* capability, or no such values array, there is no closed set and every
|
|
1347
|
+
* `vaultMode` is refused (fail closed);
|
|
1348
|
+
* (c) the declaring capability must be `vault` or list `vault` in `requires`,
|
|
1349
|
+
* so a filtered hook can only exist where the vault gate's closure holds.
|
|
1350
|
+
*
|
|
1351
|
+
* Presence is own-property presence, so a JSON `null` is refused under (b).
|
|
1352
|
+
*
|
|
1353
|
+
* @param {Map<string, object>} capMap
|
|
1354
|
+
* @returns {string[]} error strings; empty = all pass
|
|
1355
|
+
*/
|
|
1356
|
+
function validateVaultModeField(capMap) {
|
|
1357
|
+
const errors = [];
|
|
1358
|
+
const vaultCap = capMap.get('vault');
|
|
1359
|
+
const modeDecl = vaultCap && vaultCap.config && typeof vaultCap.config === 'object'
|
|
1360
|
+
? vaultCap.config['vault.mode']
|
|
1361
|
+
: undefined;
|
|
1362
|
+
const allowed = modeDecl && Array.isArray(modeDecl.values)
|
|
1363
|
+
? modeDecl.values.filter((v) => typeof v === 'string')
|
|
1364
|
+
: null;
|
|
1365
|
+
const KINDS = [['steps', 'step'], ['contributions', 'contribution'], ['gates', 'gate']];
|
|
1366
|
+
for (const [capId, cap] of capMap) {
|
|
1367
|
+
if (!cap || typeof cap !== 'object') continue;
|
|
1368
|
+
const prefix = 'capability "' + capId + '"';
|
|
1369
|
+
const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
|
|
1370
|
+
for (const [arrKey, kind] of KINDS) {
|
|
1371
|
+
const arr = Array.isArray(cap[arrKey]) ? cap[arrKey] : [];
|
|
1372
|
+
for (const hook of arr) {
|
|
1373
|
+
if (!hook || typeof hook !== 'object') continue;
|
|
1374
|
+
if (!Object.prototype.hasOwnProperty.call(hook, 'vaultMode')) continue;
|
|
1375
|
+
const where = prefix + ' ' + kind + ' at point "' + String(hook.point) + '"';
|
|
1376
|
+
const value = hook.vaultMode;
|
|
1377
|
+
if (kind === 'step') {
|
|
1378
|
+
errors.push(where + ': vaultMode is allowed on contributions and gates only');
|
|
1379
|
+
}
|
|
1380
|
+
if (allowed === null) {
|
|
1381
|
+
errors.push(
|
|
1382
|
+
where + ': vaultMode ' + JSON.stringify(value) + ' cannot be checked: the vault capability ' +
|
|
1383
|
+
'declares no "vault.mode" values, which own the closed set',
|
|
1384
|
+
);
|
|
1385
|
+
} else if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
1386
|
+
errors.push(
|
|
1387
|
+
where + ': vaultMode ' + JSON.stringify(value) + ' is not one of [' + allowed.join(', ') +
|
|
1388
|
+
'] (the closed set owned by the vault capability\'s "vault.mode" values)',
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
if (capId !== 'vault' && !requiresVault) {
|
|
1392
|
+
errors.push(
|
|
1393
|
+
where + ': vaultMode is allowed only on the "vault" capability or a capability that ' +
|
|
1394
|
+
'lists "vault" in requires',
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return errors;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Quick 261015-ect (QD-13): the vault-family rule for `requiresPlatformBindings`,
|
|
1405
|
+
* the mirror of the vaultMode rule (c) above. A capability that is `vault` or
|
|
1406
|
+
* lists `vault` in `requires` MUST declare the field, so a vault-family
|
|
1407
|
+
* capability added later cannot render hooks on a project the vault does not
|
|
1408
|
+
* govern by forgetting it. A capability outside the family may declare it too.
|
|
1409
|
+
*
|
|
1410
|
+
* @param {Map<string, object>} capMap
|
|
1411
|
+
* @returns {string[]} error strings; empty = all pass
|
|
1412
|
+
*/
|
|
1413
|
+
function validatePlatformBindingsField(capMap) {
|
|
1414
|
+
const errors = [];
|
|
1415
|
+
for (const [capId, cap] of capMap) {
|
|
1416
|
+
if (!cap || typeof cap !== 'object') continue;
|
|
1417
|
+
const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
|
|
1418
|
+
if (capId !== 'vault' && !requiresVault) continue;
|
|
1419
|
+
if (!Object.prototype.hasOwnProperty.call(cap, 'requiresPlatformBindings')) {
|
|
1420
|
+
errors.push(
|
|
1421
|
+
'capability "' + capId + '" is vault or requires vault and must declare requiresPlatformBindings, ' +
|
|
1422
|
+
'so a vault-family capability cannot render hooks on a project the vault does not govern',
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return errors;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Phase 16 D-19: the per-hook seal field's name. The resolver reads it as
|
|
1431
|
+
* `SEAL_FIELD` from capability-state; this generator cannot require that module
|
|
1432
|
+
* (its load graph reaches surface.cjs and config-loader.cjs, which read the
|
|
1433
|
+
* very registry this script regenerates), so the literal is restated here and
|
|
1434
|
+
* pinned equal to `SEAL_FIELD` by tests/capability-registry.test.cjs.
|
|
1435
|
+
*/
|
|
1436
|
+
const SEAL_FIELD_NAME = 'onRecordedArtifacts';
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Phase 16 D-19 / D-25 / D-26: validate the per-hook seal field.
|
|
1440
|
+
*
|
|
1441
|
+
* A gate carrying the field renders for an enabled, config-inactive capability
|
|
1442
|
+
* over recorded vault artifacts, bypassing the capability's active check and
|
|
1443
|
+
* its `when` (D-18). That is a widening of render behaviour, so the field is
|
|
1444
|
+
* closed HERE, at generation, to exactly the shape the two shipped seal gates
|
|
1445
|
+
* have. Six rules, each reported on its own:
|
|
1446
|
+
* (a) gates only (a step or contribution carrying it is refused);
|
|
1447
|
+
* (b) the value is literal `true` (`"true"`, `1`, `null`, `false` refused);
|
|
1448
|
+
* (c) the declaring capability lists `vault` in `requires` and is not `vault`
|
|
1449
|
+
* itself (D-26: with the vault off the injected mode is null, so both
|
|
1450
|
+
* modes' filtered gates pass and sealing the vault capability too would
|
|
1451
|
+
* dispatch `check vault.trace` twice);
|
|
1452
|
+
* (d) the gate also carries `vaultMode` (the resolver filters by mode BEFORE
|
|
1453
|
+
* the predicate, research C-1);
|
|
1454
|
+
* (e) the gate is `blocking: true`;
|
|
1455
|
+
* (f) the point is `ship:pre` (D-21: the requirement's block point; a seal at
|
|
1456
|
+
* verify:post would halt execute-phase on a switch set on purpose).
|
|
1457
|
+
*
|
|
1458
|
+
* Presence is own-property presence.
|
|
1459
|
+
*
|
|
1460
|
+
* @param {Map<string, object>} capMap
|
|
1461
|
+
* @returns {string[]} error strings; empty = all pass
|
|
1462
|
+
*/
|
|
1463
|
+
function validateSealField(capMap) {
|
|
1464
|
+
const errors = [];
|
|
1465
|
+
const KINDS = [['steps', 'step'], ['contributions', 'contribution'], ['gates', 'gate']];
|
|
1466
|
+
for (const [capId, cap] of capMap) {
|
|
1467
|
+
if (!cap || typeof cap !== 'object') continue;
|
|
1468
|
+
const prefix = 'capability "' + capId + '"';
|
|
1469
|
+
const requiresVault = Array.isArray(cap.requires) && cap.requires.includes('vault');
|
|
1470
|
+
for (const [arrKey, kind] of KINDS) {
|
|
1471
|
+
const arr = Array.isArray(cap[arrKey]) ? cap[arrKey] : [];
|
|
1472
|
+
for (const hook of arr) {
|
|
1473
|
+
if (!hook || typeof hook !== 'object') continue;
|
|
1474
|
+
if (!Object.prototype.hasOwnProperty.call(hook, SEAL_FIELD_NAME)) continue;
|
|
1475
|
+
const where = prefix + ' ' + kind + ' at point "' + String(hook.point) + '"';
|
|
1476
|
+
const value = hook[SEAL_FIELD_NAME];
|
|
1477
|
+
if (kind !== 'gate') {
|
|
1478
|
+
errors.push(where + ': ' + SEAL_FIELD_NAME + ' is allowed on gates only');
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
if (value !== true) {
|
|
1482
|
+
errors.push(where + ': ' + SEAL_FIELD_NAME + ' must be literal true (got ' + JSON.stringify(value) + ')');
|
|
1483
|
+
}
|
|
1484
|
+
if (capId === 'vault' || !requiresVault) {
|
|
1485
|
+
errors.push(
|
|
1486
|
+
where + ': ' + SEAL_FIELD_NAME + ' is allowed only on a capability that lists "vault" in requires ' +
|
|
1487
|
+
'and is not "vault" itself',
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
if (!Object.prototype.hasOwnProperty.call(hook, 'vaultMode')) {
|
|
1491
|
+
errors.push(where + ': a gate carrying ' + SEAL_FIELD_NAME + ' must also carry vaultMode');
|
|
1492
|
+
}
|
|
1493
|
+
if (hook.blocking !== true) {
|
|
1494
|
+
errors.push(where + ': a gate carrying ' + SEAL_FIELD_NAME + ' must be blocking: true');
|
|
1495
|
+
}
|
|
1496
|
+
if (hook.point !== 'ship:pre') {
|
|
1497
|
+
errors.push(where + ': ' + SEAL_FIELD_NAME + ' is allowed only at point "ship:pre"');
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return errors;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1333
1505
|
/**
|
|
1334
1506
|
* C1+C2: Global consumes-satisfiability validation.
|
|
1335
1507
|
*
|
|
@@ -1490,6 +1662,18 @@ const TIER_RANK = { core: 0, standard: 1, full: 2 };
|
|
|
1490
1662
|
function validateCrossCapability(capMap, centralKeys) {
|
|
1491
1663
|
const errors = [];
|
|
1492
1664
|
|
|
1665
|
+
// Phase 15 D-02: the per-hook vaultMode filter field (needs capMap to read the
|
|
1666
|
+
// vault capability's closed value set and every capability's `requires`).
|
|
1667
|
+
errors.push(...validateVaultModeField(capMap));
|
|
1668
|
+
|
|
1669
|
+
// Phase 16 D-19: the per-hook seal field (needs capMap for every capability's
|
|
1670
|
+
// `requires`).
|
|
1671
|
+
errors.push(...validateSealField(capMap));
|
|
1672
|
+
|
|
1673
|
+
// Quick 261015-ect (QD-13): every vault-family capability declares
|
|
1674
|
+
// requiresPlatformBindings (needs capMap for every capability's `requires`).
|
|
1675
|
+
errors.push(...validatePlatformBindingsField(capMap));
|
|
1676
|
+
|
|
1493
1677
|
// Ownership: one owner per skill stem + agent name
|
|
1494
1678
|
const skillOwner = new Map(); // skill → capId
|
|
1495
1679
|
const agentOwner = new Map(); // agent → capId
|
|
@@ -2657,6 +2841,10 @@ module.exports = {
|
|
|
2657
2841
|
validateAgainstContract,
|
|
2658
2842
|
validateConsumesGlobal,
|
|
2659
2843
|
validateCrossCapability,
|
|
2844
|
+
validateVaultModeField,
|
|
2845
|
+
validateSealField,
|
|
2846
|
+
validatePlatformBindingsField,
|
|
2847
|
+
SEAL_FIELD_NAME,
|
|
2660
2848
|
classifyCrossErrors,
|
|
2661
2849
|
loadCentralConfigKeys,
|
|
2662
2850
|
loadAndValidate,
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Inject the canonical practice-verdict arithmetic contract into every
|
|
4
|
+
* instruction surface that states it.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS. Phase 6 verification took seven passes and found eight gaps.
|
|
7
|
+
* Every one had the same root cause: one contract restated in English on five
|
|
8
|
+
* surfaces, with nothing keeping the copies true. Two of the gaps were defects
|
|
9
|
+
* introduced while repairing another.
|
|
10
|
+
*
|
|
11
|
+
* WHY GENERATION AND NOT AN `@`-INCLUDE — checked, not assumed, because
|
|
12
|
+
* ignoring the delivering surface is exactly what produced three of those
|
|
13
|
+
* misses. `agents/pp-vault-reviewer.md` and
|
|
14
|
+
* `pp-core/references/verifier-practice-verdicts.md` carry no `@`-includes; the
|
|
15
|
+
* text must be physically present for the agent reading them. And
|
|
16
|
+
* `steps/verifier-practice-payload.md` is interpolated VERBATIM into
|
|
17
|
+
* pp-verifier's prompt (execute-phase.md:1358 -> :1382), so an `@` line there
|
|
18
|
+
* would reach the agent as literal text. Physical copies are required, so this
|
|
19
|
+
* follows the repo's existing answer for required-but-must-not-drift copies:
|
|
20
|
+
* `gen-capability-registry`, `gen-platform-bindings-schema --check`.
|
|
21
|
+
*
|
|
22
|
+
* NOT A TARGET: `pp-core/templates/VAULT-REVIEW.md`. Its prose is a worked
|
|
23
|
+
* sample the agent ADAPTS, not normative text — verified by diffing the
|
|
24
|
+
* template against an artifact a reviewer actually wrote, where the wording
|
|
25
|
+
* legitimately differs. It stays guarded mechanically instead, by the
|
|
26
|
+
* arithmetic sweep in `tests/vault-review.test.cjs`.
|
|
27
|
+
*
|
|
28
|
+
* THE UNIT OF GOVERNANCE IS THE FILE, NOT THE REGION (06-16, gap G10).
|
|
29
|
+
* Verification pass 8 reproduced the hole this originally left: it replaced
|
|
30
|
+
* `verifier-practice-verdicts.md:98-100` — thirty lines ABOVE that file's own
|
|
31
|
+
* generated block, outside every marker — with the contract's exact inverse,
|
|
32
|
+
* and `--check`, `lint:ci` and the suite all stayed green. Marker-region
|
|
33
|
+
* equality says nothing about the rest of the file, and an agent reads a file
|
|
34
|
+
* linearly, so the hand-written copy is the one it meets first.
|
|
35
|
+
*
|
|
36
|
+
* So `--check` also runs a RESIDUE SCAN: for each target, the text outside its
|
|
37
|
+
* region is scanned for the contract's load-bearing needles. The needle list is
|
|
38
|
+
* NOT hardcoded here — it lives in a second marker region in the same single
|
|
39
|
+
* source, so the guard cannot drift from the contract it guards. One file owns
|
|
40
|
+
* both the canonical text and what counts as restating it.
|
|
41
|
+
*
|
|
42
|
+
* THE SCAN GOVERNS TEXT, NOT LINE BREAKS (06-17, W13). It runs the needles
|
|
43
|
+
* twice: once per raw line, and once over joined normalised windows, reporting
|
|
44
|
+
* the second as its own class only where no single line in the window matches.
|
|
45
|
+
* Verification pass 9 measured why: the per-line pass alone was defeated by an
|
|
46
|
+
* ordinary markdown wrap, using nothing but the needles' own phrases. See
|
|
47
|
+
* `scanResidue`.
|
|
48
|
+
*
|
|
49
|
+
* `--write` deliberately does NOT scan. Generation injects; it must never
|
|
50
|
+
* delete a surface's prose on the strength of a regex.
|
|
51
|
+
*
|
|
52
|
+
* Usage: --write (inject) | --check (exit 1 on drift OR residue, for lint:ci)
|
|
53
|
+
*
|
|
54
|
+
* Exit codes use `process.exitCode` + `return`, never `process.exit()`: the
|
|
55
|
+
* repo's `n/no-process-exit` rule forbids the latter, and the lint scripts here
|
|
56
|
+
* already use this form.
|
|
57
|
+
*/
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const path = require('path');
|
|
60
|
+
const crypto = require('crypto');
|
|
61
|
+
|
|
62
|
+
const ROOT = path.join(__dirname, '..');
|
|
63
|
+
const SOURCE = path.join(ROOT, 'pp-core', 'references', 'practice-arithmetic-contract.md');
|
|
64
|
+
const ALLOWLIST = path.join(ROOT, 'scripts', 'practice-arithmetic-contract.allow.json');
|
|
65
|
+
const S_START = '<!-- gen:practice-arithmetic-contract:source:start -->';
|
|
66
|
+
const S_END = '<!-- gen:practice-arithmetic-contract:source:end -->';
|
|
67
|
+
const N_START = '<!-- gen:practice-arithmetic-contract:needles:start -->';
|
|
68
|
+
const N_END = '<!-- gen:practice-arithmetic-contract:needles:end -->';
|
|
69
|
+
const START = '<!-- gen:practice-arithmetic-contract:start -->';
|
|
70
|
+
const END = '<!-- gen:practice-arithmetic-contract:end -->';
|
|
71
|
+
|
|
72
|
+
/** The four INSTRUCTION surfaces. The template is deliberately absent (see above). */
|
|
73
|
+
const TARGETS = [
|
|
74
|
+
'pp-core/workflows/vault-review.md',
|
|
75
|
+
'agents/pp-vault-reviewer.md',
|
|
76
|
+
'pp-core/references/verifier-practice-verdicts.md',
|
|
77
|
+
'pp-core/workflows/execute-phase/steps/verifier-practice-payload.md',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function readCanonical() {
|
|
81
|
+
const text = fs.readFileSync(SOURCE, 'utf-8');
|
|
82
|
+
const a = text.indexOf(S_START);
|
|
83
|
+
const b = text.indexOf(S_END);
|
|
84
|
+
if (a === -1 || b === -1 || b < a) {
|
|
85
|
+
throw new Error(`${SOURCE}: source markers missing or inverted — cannot generate`);
|
|
86
|
+
}
|
|
87
|
+
const body = text.slice(a + S_START.length, b).trim();
|
|
88
|
+
if (!body) throw new Error(`${SOURCE}: canonical block is empty — refusing to blank every target`);
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The residue needles, read from the SAME source file as the canonical text and
|
|
94
|
+
* with the SAME empty-block refusal. A needle line is `- ` + a backtick-quoted
|
|
95
|
+
* regex source + ` — ` + why it is load-bearing; the description may itself
|
|
96
|
+
* contain backticks, so only the FIRST backtick span is the pattern.
|
|
97
|
+
*
|
|
98
|
+
* A non-blank line that does not parse is a hard error. A silently skipped
|
|
99
|
+
* needle is a guard that has quietly stopped guarding, which is the whole class
|
|
100
|
+
* this file exists to end.
|
|
101
|
+
*/
|
|
102
|
+
function readNeedles() {
|
|
103
|
+
const text = fs.readFileSync(SOURCE, 'utf-8');
|
|
104
|
+
const a = text.indexOf(N_START);
|
|
105
|
+
const b = text.indexOf(N_END);
|
|
106
|
+
if (a === -1 || b === -1 || b < a) {
|
|
107
|
+
throw new Error(`${SOURCE}: needle markers missing or inverted — cannot scan for residue`);
|
|
108
|
+
}
|
|
109
|
+
const body = text.slice(a + N_START.length, b).trim();
|
|
110
|
+
if (!body) throw new Error(`${SOURCE}: the needle list is empty — refusing to scan for nothing`);
|
|
111
|
+
|
|
112
|
+
const needles = [];
|
|
113
|
+
for (const raw of body.split(/\r?\n/)) {
|
|
114
|
+
const line = raw.trim();
|
|
115
|
+
if (!line) continue;
|
|
116
|
+
const m = /^-\s+`([^`]+)`\s+—\s+(\S.*)$/.exec(line);
|
|
117
|
+
if (!m) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`${SOURCE}: unparseable needle line — expected "- \`<regex>\` — <why>", got:\n ${line}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
let re;
|
|
123
|
+
try {
|
|
124
|
+
re = new RegExp(m[1], 'i');
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw new Error(`${SOURCE}: needle \`${m[1]}\` is not a valid regex — ${err.message}`);
|
|
127
|
+
}
|
|
128
|
+
needles.push({ source: m[1], re, why: m[2] });
|
|
129
|
+
}
|
|
130
|
+
if (!needles.length) throw new Error(`${SOURCE}: the needle list parsed to zero needles`);
|
|
131
|
+
return needles;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sha256 = (s) => crypto.createHash('sha256').update(s, 'utf-8').digest('hex');
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Allowlist entries are keyed by target path and hash the EXACT allowed line.
|
|
138
|
+
* Content-hashing is `06-02`'s mechanism for the protected `pp-vault-audit`
|
|
139
|
+
* sites: editing an allowed line breaks its entry, so an allowance can never
|
|
140
|
+
* quietly widen to cover text nobody justified. A `reason` is mandatory.
|
|
141
|
+
*
|
|
142
|
+
* An ABSENT allowlist means nothing is allowed — the fail-closed direction.
|
|
143
|
+
*/
|
|
144
|
+
function readAllowlist() {
|
|
145
|
+
if (!fs.existsSync(ALLOWLIST)) return {};
|
|
146
|
+
const raw = JSON.parse(fs.readFileSync(ALLOWLIST, 'utf-8'));
|
|
147
|
+
const out = {};
|
|
148
|
+
for (const [file, entries] of Object.entries(raw)) {
|
|
149
|
+
if (file.startsWith('//')) continue; // a comment key, never a path
|
|
150
|
+
if (!TARGETS.includes(file)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`${ALLOWLIST}: "${file}" is not one of the generated targets. ` +
|
|
153
|
+
'A stale key means the allowlist outlived the file it excused — re-justify or delete it.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(entries)) throw new Error(`${ALLOWLIST}: "${file}" must map to an array`);
|
|
157
|
+
for (const e of entries) {
|
|
158
|
+
if (!/^[0-9a-f]{64}$/.test(String(e && e.sha256))) {
|
|
159
|
+
throw new Error(`${ALLOWLIST}: "${file}" has an entry without a 64-hex sha256`);
|
|
160
|
+
}
|
|
161
|
+
if (!String((e && e.reason) || '').trim()) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${ALLOWLIST}: "${file}" entry ${e.sha256.slice(0, 12)} has no reason. ` +
|
|
164
|
+
'An unjustified allowance is a rubber stamp; state why the line is not a restatement.',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
out[file] = entries;
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Adjacent non-blank lines outside the region, joined with a single space and
|
|
175
|
+
* whitespace-collapsed. A markdown paragraph is one window.
|
|
176
|
+
*
|
|
177
|
+
* Joining a whole run rather than sliding a 2-line window is not a wider net:
|
|
178
|
+
* the needles are contiguous phrases, so the only adjacencies a join creates
|
|
179
|
+
* are the ones at line boundaries — exactly the adjacencies the wrap destroyed.
|
|
180
|
+
* A run simply also covers a phrase a reflow spread over three lines.
|
|
181
|
+
*/
|
|
182
|
+
function normalisedWindows(lines, inRegion) {
|
|
183
|
+
const windows = [];
|
|
184
|
+
let run = [];
|
|
185
|
+
const flush = () => {
|
|
186
|
+
// A one-line run says nothing the per-line pass did not already say.
|
|
187
|
+
if (run.length > 1) windows.push(run);
|
|
188
|
+
run = [];
|
|
189
|
+
};
|
|
190
|
+
lines.forEach((line, i) => {
|
|
191
|
+
if (inRegion(i) || !line.trim()) {
|
|
192
|
+
flush();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
run.push({ line, i });
|
|
196
|
+
});
|
|
197
|
+
flush();
|
|
198
|
+
return windows;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Scan one target's text OUTSIDE its generated region. Returns findings and the
|
|
203
|
+
* set of allowlist hashes this file actually used, so an entry that matches no
|
|
204
|
+
* line can be reported as rotted rather than sitting there looking like cover.
|
|
205
|
+
*
|
|
206
|
+
* TWO PASSES, and the second is additive (06-17, W13).
|
|
207
|
+
*
|
|
208
|
+
* The first tests each raw LINE, which is what shipped and what the three
|
|
209
|
+
* allowlist entries hash. Verification pass 9 measured its ceiling: an ordinary
|
|
210
|
+
* markdown wrap defeats every multi-word needle, including the needles' OWN
|
|
211
|
+
* literal phrases — `two count` ending a line and `populations` opening the
|
|
212
|
+
* next scanned clean, while the same words on one line failed naming all four.
|
|
213
|
+
* The guard governed line breaks where it must govern text. Pass 9 probed all
|
|
214
|
+
* four targets and found 0 live instances, so this was latent coverage rather
|
|
215
|
+
* than residue at HEAD; the trigger, though, is an accidental prose reflow and
|
|
216
|
+
* the direction of failure is silent.
|
|
217
|
+
*
|
|
218
|
+
* So the second pass runs the same needles over joined normalised WINDOWS, and
|
|
219
|
+
* reports a hit only when the needle matches the window and matches NO single
|
|
220
|
+
* line within it. That makes a wrap-only match its own named class instead of
|
|
221
|
+
* folding it into the per-line one, and it leaves per-line semantics — and
|
|
222
|
+
* therefore the existing allowlist hashes — exactly as they were.
|
|
223
|
+
*
|
|
224
|
+
* A wrap-only finding is allowlistable like any other, but its hash is of the
|
|
225
|
+
* JOINED NORMALISED WINDOW, not of a raw line, and `--check` prints that exact
|
|
226
|
+
* text beside the hash so nobody has to reconstruct it. That digest is stable
|
|
227
|
+
* under a reflow of the same words, which is the right behaviour: what somebody
|
|
228
|
+
* justified is the sentence, not where its line breaks happened to fall.
|
|
229
|
+
*/
|
|
230
|
+
function scanResidue(file, needles, allowed) {
|
|
231
|
+
const abs = path.join(ROOT, file);
|
|
232
|
+
// Split on /\r?\n/, not '\n': the allowlist hashes the EXACT line, so a
|
|
233
|
+
// CRLF checkout must not produce a different digest for the same text.
|
|
234
|
+
const lines = fs.readFileSync(abs, 'utf-8').split(/\r?\n/);
|
|
235
|
+
const a = lines.findIndex((l) => l.includes(START));
|
|
236
|
+
const b = lines.findIndex((l) => l.includes(END));
|
|
237
|
+
const inRegion = (i) => a !== -1 && b >= a && i >= a && i <= b;
|
|
238
|
+
|
|
239
|
+
const allowedHashes = new Map((allowed[file] || []).map((e) => [e.sha256, e.reason]));
|
|
240
|
+
const used = new Set();
|
|
241
|
+
const findings = [];
|
|
242
|
+
const wrapped = [];
|
|
243
|
+
|
|
244
|
+
lines.forEach((line, i) => {
|
|
245
|
+
if (inRegion(i)) return;
|
|
246
|
+
const hits = needles.filter((n) => n.re.test(line));
|
|
247
|
+
if (!hits.length) return;
|
|
248
|
+
const h = sha256(line);
|
|
249
|
+
if (allowedHashes.has(h)) {
|
|
250
|
+
used.add(h);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
findings.push({ file, line: i + 1, text: line, hits: hits.map((n) => n.source), sha256: h });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
for (const run of normalisedWindows(lines, inRegion)) {
|
|
257
|
+
const text = run
|
|
258
|
+
.map((r) => r.line.trim())
|
|
259
|
+
.join(' ')
|
|
260
|
+
.replace(/\s+/g, ' ');
|
|
261
|
+
// `n.re.test(r.line)` against the RAW line, exactly as the first pass tests
|
|
262
|
+
// it, so "wrap-only" means precisely "the first pass could not have seen
|
|
263
|
+
// this" — including when the first pass saw it and the allowlist excused it.
|
|
264
|
+
const hits = needles.filter((n) => n.re.test(text) && !run.some((r) => n.re.test(r.line)));
|
|
265
|
+
if (!hits.length) continue;
|
|
266
|
+
const h = sha256(text);
|
|
267
|
+
if (allowedHashes.has(h)) {
|
|
268
|
+
used.add(h);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
wrapped.push({
|
|
272
|
+
file,
|
|
273
|
+
line: run[0].i + 1,
|
|
274
|
+
endLine: run[run.length - 1].i + 1,
|
|
275
|
+
text,
|
|
276
|
+
hits: hits.map((n) => n.source),
|
|
277
|
+
sha256: h,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const unused = [...allowedHashes.keys()].filter((h) => !used.has(h));
|
|
282
|
+
return { findings, wrapped, unused };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function apply(file, canonical, { write }) {
|
|
286
|
+
const abs = path.join(ROOT, file);
|
|
287
|
+
const text = fs.readFileSync(abs, 'utf-8');
|
|
288
|
+
const a = text.indexOf(START);
|
|
289
|
+
const b = text.indexOf(END);
|
|
290
|
+
if (a === -1 || b === -1) {
|
|
291
|
+
return { file, status: 'missing-markers' };
|
|
292
|
+
}
|
|
293
|
+
if (b < a) return { file, status: 'inverted-markers' };
|
|
294
|
+
|
|
295
|
+
const desired = `${START}\n${canonical}\n${END}`;
|
|
296
|
+
const current = text.slice(a, b + END.length);
|
|
297
|
+
if (current === desired) return { file, status: 'unchanged' };
|
|
298
|
+
if (write) {
|
|
299
|
+
fs.writeFileSync(abs, text.slice(0, a) + desired + text.slice(b + END.length), 'utf-8');
|
|
300
|
+
return { file, status: 'transformed' };
|
|
301
|
+
}
|
|
302
|
+
return { file, status: 'drifted' };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function main() {
|
|
306
|
+
const write = process.argv.includes('--write');
|
|
307
|
+
const check = process.argv.includes('--check');
|
|
308
|
+
if (write === check) {
|
|
309
|
+
console.error('usage: gen-practice-arithmetic-contract.cjs (--write | --check)');
|
|
310
|
+
process.exitCode = 2;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const canonical = readCanonical();
|
|
315
|
+
const results = TARGETS.map((f) => apply(f, canonical, { write }));
|
|
316
|
+
|
|
317
|
+
const bad = results.filter((r) => r.status !== 'unchanged' && r.status !== 'transformed');
|
|
318
|
+
for (const r of bad) console.error(` ${r.status}: ${r.file}`);
|
|
319
|
+
|
|
320
|
+
if (check) {
|
|
321
|
+
if (bad.length) {
|
|
322
|
+
console.error(
|
|
323
|
+
`\nFAIL: ${bad.length} surface(s) drifted from ${path.relative(ROOT, SOURCE)}.\n` +
|
|
324
|
+
'Edit that file, then run: npm run gen:practice-arithmetic-contract',
|
|
325
|
+
);
|
|
326
|
+
process.exitCode = 1;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The residue scan. Region equality is necessary and not sufficient: a
|
|
331
|
+
// surface can restate the contract by hand ANYWHERE ELSE in the same file,
|
|
332
|
+
// where the region comparison never looks. That is G10.
|
|
333
|
+
const needles = readNeedles();
|
|
334
|
+
const allowed = readAllowlist();
|
|
335
|
+
const findings = [];
|
|
336
|
+
const wrapped = [];
|
|
337
|
+
const rotted = [];
|
|
338
|
+
for (const file of TARGETS) {
|
|
339
|
+
const r = scanResidue(file, needles, allowed);
|
|
340
|
+
findings.push(...r.findings);
|
|
341
|
+
wrapped.push(...r.wrapped);
|
|
342
|
+
for (const h of r.unused) rotted.push({ file, sha256: h });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (findings.length || wrapped.length) {
|
|
346
|
+
console.error(
|
|
347
|
+
`\nFAIL: ${findings.length + wrapped.length} hand-written restatement(s) of the contract, OUTSIDE the generated region:\n`,
|
|
348
|
+
);
|
|
349
|
+
for (const f of findings) {
|
|
350
|
+
console.error(` residue: ${f.file}:${f.line} [${f.hits.join(', ')}]`);
|
|
351
|
+
console.error(` ${f.text.trim()}`);
|
|
352
|
+
}
|
|
353
|
+
// Named as its own class: the phrase is only visible once the lines are
|
|
354
|
+
// joined, so the line number is a RANGE and the hash below is of the
|
|
355
|
+
// joined text, not of any line you can see in the file (06-17, W13).
|
|
356
|
+
for (const f of wrapped) {
|
|
357
|
+
console.error(` wrapped residue: ${f.file}:${f.line}-${f.endLine} [${f.hits.join(', ')}]`);
|
|
358
|
+
console.error(` ${f.text}`);
|
|
359
|
+
}
|
|
360
|
+
console.error(
|
|
361
|
+
'\nREMEDY: replace the sentence with a POINTER to the generated block in the same file\n' +
|
|
362
|
+
'("§7 states the rule; it is not restated here"), keeping only what the canonical block\n' +
|
|
363
|
+
'does not say — a per-tier destination, for instance. The facts stay physically present\n' +
|
|
364
|
+
'in the file, in the generated block the agent also reads.\n' +
|
|
365
|
+
`If the line genuinely restates nothing, allowlist it in ${path.relative(ROOT, ALLOWLIST)}\n` +
|
|
366
|
+
'with its sha256 and a stated reason:\n',
|
|
367
|
+
);
|
|
368
|
+
for (const f of findings) console.error(` "${f.file}": [{ "sha256": "${f.sha256}", "reason": "…" }]`);
|
|
369
|
+
if (wrapped.length) {
|
|
370
|
+
console.error(
|
|
371
|
+
'\nA WRAPPED finding is hashed over the JOINED, whitespace-collapsed text printed above —\n' +
|
|
372
|
+
'not over any single line of the file. The digest therefore survives a reflow of the same\n' +
|
|
373
|
+
'words, because what an allowance justifies is the sentence, not its line breaks:\n',
|
|
374
|
+
);
|
|
375
|
+
for (const f of wrapped) console.error(` "${f.file}": [{ "sha256": "${f.sha256}", "reason": "…" }]`);
|
|
376
|
+
}
|
|
377
|
+
process.exitCode = 1;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (rotted.length) {
|
|
382
|
+
console.error(
|
|
383
|
+
`\nFAIL: ${rotted.length} allowlist entr(ies) match no line or window in their file:\n`,
|
|
384
|
+
);
|
|
385
|
+
for (const r of rotted) console.error(` rotted: ${r.file} ${r.sha256}`);
|
|
386
|
+
console.error(
|
|
387
|
+
'\nThe allowed line was edited or removed. Re-justify the new line or delete the entry —\n' +
|
|
388
|
+
'an allowance nobody can point at is exactly the rubber stamp content-hashing prevents.',
|
|
389
|
+
);
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const allowCount = Object.values(allowed).reduce((n, e) => n + e.length, 0);
|
|
395
|
+
console.log(
|
|
396
|
+
`ok practice-arithmetic-contract: ${results.length} surface(s) match the source; ` +
|
|
397
|
+
`no residue outside the regions, per line or across a wrap (${needles.length} needles, ` +
|
|
398
|
+
`${allowCount} allowlisted).`,
|
|
399
|
+
);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const t = results.filter((r) => r.status === 'transformed').length;
|
|
404
|
+
const u = results.filter((r) => r.status === 'unchanged').length;
|
|
405
|
+
if (bad.length) {
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
console.log(`Done. ${t} transformed, ${u} unchanged.`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
main();
|