@event4u/agent-config 14.13.0 → 14.14.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -62,6 +62,12 @@ import {
|
|
|
62
62
|
clone_candidate,
|
|
63
63
|
} from './bench_ab_clone.js';
|
|
64
64
|
import { main as integrity_main } from './bench_ab_integrity.js';
|
|
65
|
+
import {
|
|
66
|
+
runCascade,
|
|
67
|
+
CHEAPEST_STAGE,
|
|
68
|
+
type CascadeResult,
|
|
69
|
+
} from './_lib/evaluation_cascade.js';
|
|
70
|
+
import type { MetricVector } from './_lib/evaluation_vector.js';
|
|
65
71
|
import {
|
|
66
72
|
type CandidateRecord,
|
|
67
73
|
CandidateSchemaError,
|
|
@@ -76,6 +82,15 @@ import {
|
|
|
76
82
|
parseCandidateRecord,
|
|
77
83
|
readCandidateRecord,
|
|
78
84
|
} from './_lib/candidate_record.js';
|
|
85
|
+
import {
|
|
86
|
+
PROMOTION_EVIDENCE_FIELDS,
|
|
87
|
+
PromotionEvidenceError,
|
|
88
|
+
parsePromotionEvidence,
|
|
89
|
+
} from './_lib/promotion_evidence.js';
|
|
90
|
+
import { MERGE_AUTHORITY_ROADMAP, readMergeAuthorityStatus } from './_lib/promotion_capability.js';
|
|
91
|
+
import { assertNotSemanticNoOp, isSemanticNoOp, SemanticNoOpError } from './_lib/semantic_noop.js';
|
|
92
|
+
import { buildRunSpec } from './_lib/experiment_binding.js';
|
|
93
|
+
import { ExperimentDriftError, assertUnchanged, freeze } from './_lib/experiment_freeze.js';
|
|
79
94
|
import {
|
|
80
95
|
RECIPES,
|
|
81
96
|
byteCompare,
|
|
@@ -84,6 +99,13 @@ import {
|
|
|
84
99
|
proposeCandidates,
|
|
85
100
|
serialiseCandidateRecord,
|
|
86
101
|
} from './_lib/candidate_proposer.js';
|
|
102
|
+
import {
|
|
103
|
+
type RunReport,
|
|
104
|
+
buildRunReport,
|
|
105
|
+
parseMetricVectorJson,
|
|
106
|
+
renderRunReport,
|
|
107
|
+
roiFigure,
|
|
108
|
+
} from './_lib/evolution_roi.js';
|
|
87
109
|
import {
|
|
88
110
|
BudgetExceededError,
|
|
89
111
|
type DisclosureRecord,
|
|
@@ -379,11 +401,11 @@ const USAGE = `usage: evolution_lab <verb> [options]
|
|
|
379
401
|
|
|
380
402
|
inspect [--record FILE]... [--records DIR] [--clones]
|
|
381
403
|
propose --observations FILE --out DIR [--force]
|
|
382
|
-
run --record FILE... [--refresh]
|
|
404
|
+
run --record FILE... [--refresh] [--vector FILE]...
|
|
383
405
|
[--trials-per-candidate N] [--estimated-spend-cents N]
|
|
384
406
|
compare [--verbose]
|
|
385
407
|
explain [--record FILE [--to STATE]] [--criteria]
|
|
386
|
-
promote --record FILE
|
|
408
|
+
promote --record FILE [--evidence FILE] (always refuses: blocker merge-authority)
|
|
387
409
|
clean [--yes]
|
|
388
410
|
|
|
389
411
|
No verb starts a resident process, a timer, or a watcher.
|
|
@@ -615,6 +637,15 @@ function guardAbort(e: unknown): number | undefined {
|
|
|
615
637
|
);
|
|
616
638
|
return EXIT_GUARD_ABORT;
|
|
617
639
|
}
|
|
640
|
+
if (e instanceof ExperimentDriftError) {
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`evolution_lab: ABORTED on experiment drift (moved: ${e.changed.join(', ')})\n` +
|
|
643
|
+
` ${e.message}\n` +
|
|
644
|
+
' The run cloned one record set and would have scored another. Nothing is\n' +
|
|
645
|
+
' reported: an ROI figure over a set that moved mid-run is not a comparison.\n',
|
|
646
|
+
);
|
|
647
|
+
return EXIT_GUARD_ABORT;
|
|
648
|
+
}
|
|
618
649
|
if (e instanceof BudgetConfigError) {
|
|
619
650
|
process.stderr.write(`evolution_lab: ABORTED — ${(e as Error).message}\n`);
|
|
620
651
|
return EXIT_GUARD_ABORT;
|
|
@@ -698,7 +729,7 @@ function verbRun(argv: readonly string[]): number {
|
|
|
698
729
|
const flags = parseFlags(
|
|
699
730
|
argv,
|
|
700
731
|
['refresh'],
|
|
701
|
-
['record', 'records', 'trials-per-candidate', 'estimated-spend-cents'],
|
|
732
|
+
['record', 'records', 'trials-per-candidate', 'estimated-spend-cents', 'vector'],
|
|
702
733
|
);
|
|
703
734
|
const files: string[] = [...(flags.values.get('record') ?? [])];
|
|
704
735
|
const dir = one(flags, 'records');
|
|
@@ -734,8 +765,51 @@ function verbRun(argv: readonly string[]): number {
|
|
|
734
765
|
}
|
|
735
766
|
throw e;
|
|
736
767
|
}
|
|
768
|
+
// Evaluation evidence is parsed BEFORE the first clone, for the same reason
|
|
769
|
+
// the budget guard runs before it: a malformed vector discovered after the
|
|
770
|
+
// work is a failed run that already spent, and `parseMetricVectorJson`
|
|
771
|
+
// inherits `buildVector`'s refusal of a vector missing its artifact-count
|
|
772
|
+
// row, so this is also where that refusal lands.
|
|
773
|
+
const vectors: MetricVector[] = [];
|
|
774
|
+
for (const vf of flags.values.get('vector') ?? []) {
|
|
775
|
+
try {
|
|
776
|
+
vectors.push(parseMetricVectorJson(fs.readFileSync(vf, 'utf-8'), vf));
|
|
777
|
+
} catch (e) {
|
|
778
|
+
return fail(`vector ${vf} rejected — ${(e as Error).message}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// The experiment binding. Frozen HERE, before the first clone, and asserted
|
|
782
|
+
// again after the loop. The window it closes is real and specific: the loop
|
|
783
|
+
// below clones from `record`, then RE-READS the same file from disk to
|
|
784
|
+
// evaluate it, so a record rewritten in between produces a run that cloned
|
|
785
|
+
// one thing and scored another. Every guard already in this verb fires
|
|
786
|
+
// before the run starts and never looks again.
|
|
787
|
+
const ordered = [...files].sort(byteCompare);
|
|
788
|
+
const readRecord = (f: string): string => {
|
|
789
|
+
try {
|
|
790
|
+
return fs.readFileSync(f, 'utf-8');
|
|
791
|
+
} catch {
|
|
792
|
+
// Unreadable is a STATE of the record set here, not a reason to
|
|
793
|
+
// fail: the loop below already reports the specific file with its
|
|
794
|
+
// own message, and a freeze that pre-empted it would replace a
|
|
795
|
+
// precise diagnostic with a vaguer one. The marker still moves the
|
|
796
|
+
// digest, so a record that appears or vanishes mid-run is drift.
|
|
797
|
+
return '\u0000<unreadable>';
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
let frozenSpec;
|
|
801
|
+
let frozenDigest: string;
|
|
802
|
+
try {
|
|
803
|
+
frozenSpec = buildRunSpec(ordered, readRecord);
|
|
804
|
+
frozenDigest = freeze(frozenSpec);
|
|
805
|
+
} catch (e) {
|
|
806
|
+
return fail(`experiment spec could not be built — ${(e as Error).message}`);
|
|
807
|
+
}
|
|
808
|
+
|
|
737
809
|
const seen = new Set<string>();
|
|
738
|
-
|
|
810
|
+
const ids: string[] = [];
|
|
811
|
+
const results: CascadeResult[] = [];
|
|
812
|
+
for (const f of ordered) {
|
|
739
813
|
let record: CandidateRecord;
|
|
740
814
|
try {
|
|
741
815
|
record = loadRecord(f);
|
|
@@ -754,6 +828,108 @@ function verbRun(argv: readonly string[]): number {
|
|
|
754
828
|
}
|
|
755
829
|
return fail(`candidate ${record.id} failed — ${(e as Error).message}`);
|
|
756
830
|
}
|
|
831
|
+
|
|
832
|
+
// EVALUATE. Step 4.1's deterministic prefix, wired here because a
|
|
833
|
+
// library nothing calls has no coverage — the defect AC-3 and AC-5
|
|
834
|
+
// were both open on. The record is re-read from disk rather than
|
|
835
|
+
// reusing `record`, so stage 1 is a real schema gate at this call
|
|
836
|
+
// site and not a formality over an already-parsed object.
|
|
837
|
+
let raw: unknown;
|
|
838
|
+
try {
|
|
839
|
+
raw = JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
840
|
+
} catch (e) {
|
|
841
|
+
return fail(`${f} unreadable at evaluation — ${(e as Error).message}`);
|
|
842
|
+
}
|
|
843
|
+
const result = runCascade({
|
|
844
|
+
raw,
|
|
845
|
+
plan: {
|
|
846
|
+
candidates: files.length,
|
|
847
|
+
trialsPerCandidate: intFlag(flags, 'trials-per-candidate', 1),
|
|
848
|
+
estimatedSpendCents: intFlag(flags, 'estimated-spend-cents', 0),
|
|
849
|
+
},
|
|
850
|
+
budget: loadRunBudget(),
|
|
851
|
+
peers: ids,
|
|
852
|
+
// ONE evidence input, two consumers. `--vector` is the only
|
|
853
|
+
// measurement flag: step 5.6's run report and step 4.1's cascade
|
|
854
|
+
// read the same parsed vectors rather than each taking a file of
|
|
855
|
+
// its own, so a run cannot report an ROI over one set of numbers
|
|
856
|
+
// while the verdict was decided on another.
|
|
857
|
+
vector: vectors.find((v) => v.candidate_id === record.id),
|
|
858
|
+
});
|
|
859
|
+
ids.push(record.id);
|
|
860
|
+
results.push(result);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Assert BEFORE the verdicts are rendered, and before the cascade-abort
|
|
864
|
+
// return below: a drifted run has no verdict worth printing, whichever way
|
|
865
|
+
// the cascade came out.
|
|
866
|
+
try {
|
|
867
|
+
assertUnchanged(frozenDigest, frozenSpec, buildRunSpec(ordered, readRecord));
|
|
868
|
+
} catch (e) {
|
|
869
|
+
const aborted = guardAbort(e);
|
|
870
|
+
if (aborted !== undefined) {
|
|
871
|
+
return aborted;
|
|
872
|
+
}
|
|
873
|
+
throw e;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
for (const r of results) {
|
|
877
|
+
if (r.outcome === 'abort') {
|
|
878
|
+
process.stdout.write(
|
|
879
|
+
`evolution_lab:cascade · ${r.candidate_id ?? '<unparsed>'} · aborted at ` +
|
|
880
|
+
`${r.failed_stage} · family=${r.family} · model_calls=${r.model_calls} · ${r.detail}\n`,
|
|
881
|
+
);
|
|
882
|
+
} else if (r.outcome === 'incomplete') {
|
|
883
|
+
process.stdout.write(
|
|
884
|
+
`evolution_lab:cascade · ${r.candidate_id} · passed ${r.stages_run.length} stage(s) · ` +
|
|
885
|
+
`model_calls=${r.model_calls} · ${r.not_reached} NOT REACHED · ${r.why}\n`,
|
|
886
|
+
);
|
|
887
|
+
} else {
|
|
888
|
+
process.stdout.write(
|
|
889
|
+
`evolution_lab:cascade · ${r.candidate_id} · passed ${r.stages_run.length} stage(s) · ` +
|
|
890
|
+
`model_calls=${r.model_calls} · verdict=${r.verdict.promote ? 'promote' : 'refuse'} · ` +
|
|
891
|
+
`${r.verdict.reason}\n`,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// A cascade abort is a run outcome, not a crash: the run did its job by
|
|
897
|
+
// refusing. The cheapest stage is named so a reader can see that a stage-1
|
|
898
|
+
// abort cost nothing.
|
|
899
|
+
const aborted = results.filter((r) => r.outcome === 'abort');
|
|
900
|
+
if (aborted.length > 0) {
|
|
901
|
+
const atCheapest = aborted.filter(
|
|
902
|
+
(r) => r.outcome === 'abort' && r.failed_stage === CHEAPEST_STAGE,
|
|
903
|
+
).length;
|
|
904
|
+
process.stdout.write(
|
|
905
|
+
`evolution_lab:cascade · ${aborted.length} of ${results.length} aborted ` +
|
|
906
|
+
`(${atCheapest} at the cheapest stage, ${CHEAPEST_STAGE}, costing no model call)\n`,
|
|
907
|
+
);
|
|
908
|
+
return EXIT_REFUSED;
|
|
909
|
+
}
|
|
910
|
+
// STEP 5.6 — the run report, on the ONE path a run completes on.
|
|
911
|
+
//
|
|
912
|
+
// Placed after the clone loop and before the only success return, so there
|
|
913
|
+
// is no completed run without a report. Every other exit from this verb is
|
|
914
|
+
// an abort (budget) or a failure (unreadable record), and neither is a run
|
|
915
|
+
// whose ROI could be reported: nothing was cloned and nothing was spent.
|
|
916
|
+
let report: RunReport;
|
|
917
|
+
try {
|
|
918
|
+
report = buildRunReport({
|
|
919
|
+
// Deterministic and identifying: the candidate ids, in the byte
|
|
920
|
+
// order the run walked them. Two runs over the same record set
|
|
921
|
+
// produce the same id, and a run over a different set cannot
|
|
922
|
+
// borrow another run's report line.
|
|
923
|
+
run_id: `run:${[...seen].sort(byteCompare).join('+')}`,
|
|
924
|
+
candidates: files.length,
|
|
925
|
+
trials_per_candidate: intFlag(flags, 'trials-per-candidate', 1),
|
|
926
|
+
roi: roiFigure(vectors, intFlag(flags, 'estimated-spend-cents', 0)),
|
|
927
|
+
});
|
|
928
|
+
} catch (e) {
|
|
929
|
+
return fail(`run report rejected — ${(e as Error).message}`);
|
|
930
|
+
}
|
|
931
|
+
for (const line of renderRunReport(report)) {
|
|
932
|
+
process.stdout.write(`${line}\n`);
|
|
757
933
|
}
|
|
758
934
|
return EXIT_OK;
|
|
759
935
|
}
|
|
@@ -854,9 +1030,15 @@ function verbExplain(argv: readonly string[]): number {
|
|
|
854
1030
|
* There is no flag, no environment variable and no argument that turns this
|
|
855
1031
|
* into a promotion. `--approver` is deliberately NOT a flag: adding one is what
|
|
856
1032
|
* would let an unattended run name a human who never approved.
|
|
1033
|
+
*
|
|
1034
|
+
* `--evidence FILE` IS a flag, and it changes nothing about the refusal. It is
|
|
1035
|
+
* how steps 7.1, 7.3, 7.4 and 7.5 are exercised against a refused promotion: the
|
|
1036
|
+
* package is validated FIRST and its finding names the missing field, so an
|
|
1037
|
+
* operator assembling one can see which field is wrong instead of only being
|
|
1038
|
+
* told the blocker is open. Both paths still return {@link EXIT_REFUSED}.
|
|
857
1039
|
*/
|
|
858
1040
|
function verbPromote(argv: readonly string[]): number {
|
|
859
|
-
const flags = parseFlags(argv, [], ['record']);
|
|
1041
|
+
const flags = parseFlags(argv, [], ['record', 'evidence']);
|
|
860
1042
|
const file = one(flags, 'record');
|
|
861
1043
|
if (file === undefined) {
|
|
862
1044
|
return usageError('promote requires --record FILE');
|
|
@@ -867,20 +1049,63 @@ function verbPromote(argv: readonly string[]): number {
|
|
|
867
1049
|
} catch (e) {
|
|
868
1050
|
return fail(`${file} rejected — ${(e as Error).message}`);
|
|
869
1051
|
}
|
|
1052
|
+
|
|
1053
|
+
// 7.1 / 7.3 / 7.4 / 7.5 — the evidence package, checked BEFORE the governance
|
|
1054
|
+
// refusal so its finding names the missing field rather than being swallowed
|
|
1055
|
+
// by a blocker message the operator cannot act on.
|
|
1056
|
+
const evidenceFile = one(flags, 'evidence');
|
|
1057
|
+
if (evidenceFile === undefined) {
|
|
1058
|
+
process.stderr.write(
|
|
1059
|
+
`evolution_lab: promote REFUSED for ${record.id}\n` +
|
|
1060
|
+
' evidence package: absent. A promotion carries ONE package with all ' +
|
|
1061
|
+
`${String(PROMOTION_EVIDENCE_FIELDS.length)} fields ` +
|
|
1062
|
+
`(${PROMOTION_EVIDENCE_FIELDS.join(', ')}); pass --evidence FILE.\n`,
|
|
1063
|
+
);
|
|
1064
|
+
} else {
|
|
1065
|
+
try {
|
|
1066
|
+
const evidence = parsePromotionEvidence(JSON.parse(fs.readFileSync(evidenceFile, 'utf-8')));
|
|
1067
|
+
assertNotSemanticNoOp(
|
|
1068
|
+
evidence.candidateId,
|
|
1069
|
+
isSemanticNoOp(
|
|
1070
|
+
evidence.materialImprovement.baselineText,
|
|
1071
|
+
evidence.materialImprovement.candidateText,
|
|
1072
|
+
evidence.materialImprovement.deltaPercent,
|
|
1073
|
+
),
|
|
1074
|
+
);
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
if (e instanceof PromotionEvidenceError || e instanceof SemanticNoOpError) {
|
|
1077
|
+
process.stderr.write(
|
|
1078
|
+
`evolution_lab: promote REFUSED for ${record.id}\n evidence package: ${e.message}\n`,
|
|
1079
|
+
);
|
|
1080
|
+
} else {
|
|
1081
|
+
return fail(`${evidenceFile} unreadable — ${(e as Error).message}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
870
1086
|
let gate = 'the lifecycle gate did not refuse, which is itself a defect';
|
|
871
1087
|
try {
|
|
872
1088
|
assertTransition(record.lifecycle, 'promoted');
|
|
873
1089
|
} catch (e) {
|
|
874
1090
|
gate = (e as Error).message;
|
|
875
1091
|
}
|
|
1092
|
+
// READ the status, never assert it. This line said `is OPEN` as a literal,
|
|
1093
|
+
// and on 2026-09-01 the blocker closed as `refused` — at which point the
|
|
1094
|
+
// refusal was still correct and its stated reason was not. Reading keeps the
|
|
1095
|
+
// two from diverging again, and every value the reader can return is a
|
|
1096
|
+
// refusing one, so no branch here can print a grant.
|
|
1097
|
+
const blockerStatus = readMergeAuthorityStatus();
|
|
876
1098
|
process.stderr.write(
|
|
877
1099
|
`evolution_lab: promote REFUSED for ${record.id}\n` +
|
|
878
1100
|
` lifecycle gate: ${gate}\n` +
|
|
879
|
-
|
|
1101
|
+
` blocker: merge-authority reads ${blockerStatus.toUpperCase()} on ` +
|
|
1102
|
+
`${path.basename(MERGE_AUTHORITY_ROADMAP, '.md')}.\n` +
|
|
880
1103
|
' Phases 1-6 are legal because they promote nothing (AI council 2026-08-29,\n' +
|
|
881
|
-
' anthropic + openai, 2/2). Phase 7
|
|
882
|
-
'
|
|
883
|
-
'
|
|
1104
|
+
' anthropic + openai, 2/2). Phase 7 was gated on this blocker; on 2026-09-01\n' +
|
|
1105
|
+
' the AI council settled ADR-239 Decision 3 in the REFUSING direction, scoped\n' +
|
|
1106
|
+
' to PREAUTHORIZED authority, and the blocker closed as `refused`. Only\n' +
|
|
1107
|
+
' `Status: resolved` AND `Disposition: granted` mint the capability, so a\n' +
|
|
1108
|
+
' refusal keeps this verb shut exactly as an open blocker did.\n' +
|
|
884
1109
|
' Promotion into canonical agent-config remains a named human act performed\n' +
|
|
885
1110
|
' outside this tool. This verb will not perform it.\n',
|
|
886
1111
|
);
|
|
@@ -72,7 +72,7 @@ interface FnSpec {
|
|
|
72
72
|
cells: Record<string, string>;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// Each condense.
|
|
75
|
+
// Each condense.ts dispatcher generator → what it emits, for which host(s),
|
|
76
76
|
// via which mechanism. Universal generators (symlink fan-out to every host
|
|
77
77
|
// that consumes the artifact) list all consuming hosts explicitly. This is the
|
|
78
78
|
// one place the host×artifact mechanism is asserted; the coverage guard below
|
|
@@ -222,10 +222,17 @@ export function render_md(matrix: Record<string, Record<string, string>>): strin
|
|
|
222
222
|
const lines: string[] = [
|
|
223
223
|
'# Capability matrix — what works on which host',
|
|
224
224
|
'',
|
|
225
|
-
'> **Generated
|
|
226
|
-
'>
|
|
227
|
-
'>
|
|
228
|
-
'>
|
|
225
|
+
'> **Generated — still generated, not hand-maintained prose wearing a',
|
|
226
|
+
'> generated header.** Emitted by `src/scripts/generate_capability_matrix.ts`;',
|
|
227
|
+
'> do NOT hand-edit. Derived from the `generate_tools()` projection logic in',
|
|
228
|
+
'> `src/scripts/condense.ts` (each cell traces to a `generate_*` dispatcher',
|
|
229
|
+
'> call). Drift-checked in CI (`--check`).',
|
|
230
|
+
'>',
|
|
231
|
+
'> Until 2026-09-03 this header named two Python-era generator paths that',
|
|
232
|
+
'> have not existed since ADR-200 ended that era, so a reader checking the',
|
|
233
|
+
'> provenance of this table found dead paths and no way to tell whether',
|
|
234
|
+
'> anything still produced it. They are described rather than reproduced',
|
|
235
|
+
'> here, so every path this header names resolves.',
|
|
229
236
|
'',
|
|
230
237
|
'Cells: **✅ native** (host consumes the artifact directly — symlink /',
|
|
231
238
|
'native dir) · **🔁 adapter** (projected through a host-specific',
|
|
@@ -337,8 +344,8 @@ interface ParsedArgs {
|
|
|
337
344
|
class _ArgExit extends Error {}
|
|
338
345
|
|
|
339
346
|
function _argError(msg: string): never {
|
|
340
|
-
process.stderr.write('usage: generate_capability_matrix
|
|
341
|
-
process.stderr.write(`generate_capability_matrix
|
|
347
|
+
process.stderr.write('usage: generate_capability_matrix [-h] [--check] [--quiet]\n');
|
|
348
|
+
process.stderr.write(`generate_capability_matrix: error: ${msg}\n`);
|
|
342
349
|
process.exitCode = 2;
|
|
343
350
|
throw new _ArgExit();
|
|
344
351
|
}
|
|
@@ -347,7 +354,7 @@ function parse_args(argv: string[]): ParsedArgs {
|
|
|
347
354
|
const out: ParsedArgs = { check: false, quiet: false };
|
|
348
355
|
for (const a of argv) {
|
|
349
356
|
if (a === '-h' || a === '--help') {
|
|
350
|
-
process.stdout.write('usage: generate_capability_matrix
|
|
357
|
+
process.stdout.write('usage: generate_capability_matrix [-h] [--check] [--quiet]\n');
|
|
351
358
|
process.exitCode = 0;
|
|
352
359
|
throw new _ArgExit();
|
|
353
360
|
} else if (a === '--check') {
|
|
@@ -383,7 +390,7 @@ export function main(argv: string[] | null = null): number {
|
|
|
383
390
|
const missing = coverage_guard();
|
|
384
391
|
if (missing.length > 0) {
|
|
385
392
|
process.stderr.write(
|
|
386
|
-
'❌ generate_capability_matrix: condense.
|
|
393
|
+
'❌ generate_capability_matrix: condense.ts dispatcher has ' +
|
|
387
394
|
`generator(s) not mapped in _FN_SPEC: ${_pyListRepr(missing)}. Add an _FN_SPEC ` +
|
|
388
395
|
'entry so the matrix stays derived (never silently drift).\n',
|
|
389
396
|
);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* written in a command's markdown cannot be asserted by anything. So the
|
|
8
8
|
* detection is here and the command invokes it; the command keeps the narrative.
|
|
9
9
|
*
|
|
10
|
-
* THE ANTI-VANITY RULE IS STRUCTURAL, NOT A STYLE NOTE.
|
|
10
|
+
* THE ANTI-VANITY RULE IS STRUCTURAL, NOT A STYLE NOTE. Thirteen dimensions are
|
|
11
11
|
* graded 0 Absent / 1 Present / 2 Enforced-in-CI / 3 Independent-and-diff-scoped,
|
|
12
12
|
* and the verdict is `min` over the FOUR knockout dimensions, reported as
|
|
13
13
|
* `L<n> — bound by <dimension>`. **No aggregate number is ever emitted** — no
|
|
@@ -113,6 +113,66 @@ function _ciText(root: string): string {
|
|
|
113
113
|
return out;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/* -- bounded text corpus, for the Phase 4 dimensions ----------------------- */
|
|
117
|
+
|
|
118
|
+
/** Directories a target scan must never descend into: cost with no signal. */
|
|
119
|
+
const _SKIP_DIRS = new Set([
|
|
120
|
+
'node_modules', '.git', 'vendor', 'dist', 'build', 'out', 'coverage',
|
|
121
|
+
'.next', '.nuxt', '.venv', 'venv', '__pycache__', 'target', '.terraform',
|
|
122
|
+
]);
|
|
123
|
+
/** Extensions that can carry a DNS record, a policy doc or an ops runbook. */
|
|
124
|
+
const _CORPUS_RE = /\.(tf|tfvars|hcl|ya?ml|json|txt|zone|dns|ini|conf|cfg|toml|md|env|example|sh)$/i;
|
|
125
|
+
const _CORPUS_MAX_FILES = 2000;
|
|
126
|
+
const _CORPUS_MAX_BYTES = 256 * 1024;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read a bounded slice of the target's text, once, for the dimensions that
|
|
130
|
+
* cannot be answered by a root-level `_exists` probe.
|
|
131
|
+
*
|
|
132
|
+
* Three dimensions need to see INSIDE files — a DMARC policy is a string in a
|
|
133
|
+
* zone file or a Terraform record, not a filename — and the existing probes are
|
|
134
|
+
* all filename-shaped. The walk is capped in three directions (skip list, file
|
|
135
|
+
* count, per-file bytes) because an unbounded read of an arbitrary target repo
|
|
136
|
+
* is a cost this grader has no way to predict.
|
|
137
|
+
*
|
|
138
|
+
* Returns the concatenated text AND the relative paths, because a dimension that
|
|
139
|
+
* says WHERE it saw something is auditable and one that only says "somewhere in
|
|
140
|
+
* the tree" is not.
|
|
141
|
+
*/
|
|
142
|
+
export function _textCorpus(root: string): { text: string; paths: string[] } {
|
|
143
|
+
const paths: string[] = [];
|
|
144
|
+
let text = '';
|
|
145
|
+
const walk = (dir: string, rel: string): void => {
|
|
146
|
+
if (paths.length >= _CORPUS_MAX_FILES) return;
|
|
147
|
+
let entries: fs.Dirent[];
|
|
148
|
+
try {
|
|
149
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
150
|
+
} catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
154
|
+
if (paths.length >= _CORPUS_MAX_FILES) return;
|
|
155
|
+
const abs = path.join(dir, e.name);
|
|
156
|
+
const r = rel === '' ? e.name : `${rel}/${e.name}`;
|
|
157
|
+
if (e.isDirectory()) {
|
|
158
|
+
if (_SKIP_DIRS.has(e.name)) continue;
|
|
159
|
+
walk(abs, r);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!_CORPUS_RE.test(e.name)) continue;
|
|
163
|
+
try {
|
|
164
|
+
if (fs.statSync(abs).size > _CORPUS_MAX_BYTES) continue;
|
|
165
|
+
text += `${fs.readFileSync(abs, 'utf-8')}\n`;
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
paths.push(r);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
walk(root, '');
|
|
173
|
+
return { text, paths };
|
|
174
|
+
}
|
|
175
|
+
|
|
116
176
|
/**
|
|
117
177
|
* Is this a Python-primary target?
|
|
118
178
|
*
|
|
@@ -125,7 +185,7 @@ export function isPythonTarget(root: string): boolean {
|
|
|
125
185
|
return py && !other;
|
|
126
186
|
}
|
|
127
187
|
|
|
128
|
-
// ── the
|
|
188
|
+
// ── the thirteen dimensions ──
|
|
129
189
|
|
|
130
190
|
/**
|
|
131
191
|
* Grade one dimension. `ci` decides 1 → 2: a thing that exists is `Present`; a
|
|
@@ -149,6 +209,113 @@ function _ciBlocks(ci: string, ...needles: string[]): boolean {
|
|
|
149
209
|
return !allSoft;
|
|
150
210
|
}
|
|
151
211
|
|
|
212
|
+
/* -- Phase 4 dimension ladders --------------------------------------------- */
|
|
213
|
+
|
|
214
|
+
interface Scored {
|
|
215
|
+
grade: Grade;
|
|
216
|
+
evidence: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A runbook, a restore procedure, or a rollback path — and whether CI runs it. */
|
|
220
|
+
function _recovery(corpus: { text: string; paths: string[] }, ci: string): Scored {
|
|
221
|
+
const namedFile = corpus.paths.some((p) => /(^|\/)(runbook|runbooks|disaster-recovery|dr)([./-]|$)/i.test(p));
|
|
222
|
+
const procedure = /\b(restore from backup|disaster recovery|rollback plan|pg_restore|pg_dump|restic|velero|RPO|RTO)\b/i.test(corpus.text);
|
|
223
|
+
const present = namedFile || procedure;
|
|
224
|
+
const enforced = _ciBlocks(ci, 'restore', 'rollback', 'disaster-recovery', 'backup');
|
|
225
|
+
// A DRILL is what separates a documented procedure from a proven one. It is
|
|
226
|
+
// the only honest route to 3 here, because "independent" for recovery means
|
|
227
|
+
// the restore was actually exercised rather than described.
|
|
228
|
+
const drill = /\b(restore (drill|test|rehearsal)|recovery drill|game ?day)\b/i.test(corpus.text);
|
|
229
|
+
const seen: string[] = [
|
|
230
|
+
...(namedFile ? ['a runbook or DR document'] : []),
|
|
231
|
+
...(procedure ? ['a restore/rollback procedure in the text'] : []),
|
|
232
|
+
...(enforced ? ['a blocking CI step naming restore/rollback/backup'] : []),
|
|
233
|
+
...(drill ? ['a restore drill'] : []),
|
|
234
|
+
];
|
|
235
|
+
return {
|
|
236
|
+
grade: _graded(present, enforced, drill),
|
|
237
|
+
evidence: seen.length > 0 ? seen.join('; ') : 'no runbook, restore procedure or rollback path found',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* SPF, DKIM and DMARC — and the rung that must not be fudged (4.2).
|
|
243
|
+
*
|
|
244
|
+
* 0 Absent — no SPF, DKIM or DMARC RECORD anywhere. Prose about DMARC is
|
|
245
|
+
* not a DMARC record; the signal is the record shape.
|
|
246
|
+
* 1 Present — SPF and/or DKIM published, OR a DMARC record with `p=none`.
|
|
247
|
+
* `p=none` is a MONITORING policy: it requests aggregate reports
|
|
248
|
+
* and instructs receivers to take no action on a failing message.
|
|
249
|
+
* A domain publishing it is measuring the problem, not preventing
|
|
250
|
+
* it, so it is capped here however much else sits alongside it.
|
|
251
|
+
* 2 Enforced — DMARC `p=quarantine` or `p=reject`, with SPF or DKIM to align
|
|
252
|
+
* against. An enforcing policy with nothing to align is a policy
|
|
253
|
+
* over no signal, and stays at 1.
|
|
254
|
+
* 3 Top — `p=reject` with BOTH SPF and DKIM and an `rua=` reporting
|
|
255
|
+
* address, so the enforcement can be observed rather than assumed.
|
|
256
|
+
*/
|
|
257
|
+
function _mailAuthenticity(corpus: { text: string }): Scored {
|
|
258
|
+
const t = corpus.text;
|
|
259
|
+
const spf = /v=spf1\b/i.test(t);
|
|
260
|
+
const dkim = /v=DKIM1\b/i.test(t);
|
|
261
|
+
const dmarc = /v=DMARC1\b/i.test(t);
|
|
262
|
+
const policy = /v=DMARC1[^"'\n]*?\bp\s*=\s*(none|quarantine|reject)\b/i.exec(t)?.[1]?.toLowerCase();
|
|
263
|
+
const rua = /v=DMARC1[^"'\n]*?\brua\s*=/i.test(t);
|
|
264
|
+
const aligned = spf || dkim;
|
|
265
|
+
const parts = [
|
|
266
|
+
spf ? 'SPF published' : 'no SPF record',
|
|
267
|
+
dkim ? 'DKIM published' : 'no DKIM record',
|
|
268
|
+
dmarc ? `DMARC published (p=${policy ?? 'unset'})` : 'no DMARC record',
|
|
269
|
+
];
|
|
270
|
+
if (!spf && !dkim && !dmarc) {
|
|
271
|
+
return { grade: 0, evidence: 'no SPF, DKIM or DMARC record found in the tree' };
|
|
272
|
+
}
|
|
273
|
+
if (policy === 'none') {
|
|
274
|
+
return {
|
|
275
|
+
grade: 1,
|
|
276
|
+
evidence:
|
|
277
|
+
`${parts.join('; ')} — capped at Present: p=none is a monitoring policy and is NOT protection, ` +
|
|
278
|
+
'because receivers are told to take no action on a message that fails. A domain publishing it ' +
|
|
279
|
+
'scores below one that enforces.',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (policy === 'quarantine' || policy === 'reject') {
|
|
283
|
+
if (!aligned) {
|
|
284
|
+
return {
|
|
285
|
+
grade: 1,
|
|
286
|
+
evidence: `${parts.join('; ')} — an enforcing policy with neither SPF nor DKIM to align against protects nothing yet`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (policy === 'reject' && spf && dkim && rua) {
|
|
290
|
+
return { grade: 3, evidence: `${parts.join('; ')} — enforcing, both mechanisms aligned, and reporting to an rua address` };
|
|
291
|
+
}
|
|
292
|
+
return { grade: 2, evidence: `${parts.join('; ')} — an enforcing DMARC policy with an aligned mechanism` };
|
|
293
|
+
}
|
|
294
|
+
return { grade: 1, evidence: `${parts.join('; ')} — a mail-authenticity record exists, with no enforcing DMARC policy above it` };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A privacy notice, a retention rule, a consent mechanism, a deletion path. */
|
|
298
|
+
function _privacyObligations(corpus: { text: string; paths: string[] }, ci: string): Scored {
|
|
299
|
+
const notice =
|
|
300
|
+
corpus.paths.some((p) => /(privacy|datenschutz|\bdpa\b|data-processing)/i.test(p)) ||
|
|
301
|
+
/\b(privacy (policy|notice)|Datenschutzerkl)/i.test(corpus.text);
|
|
302
|
+
const retention = /\b(retention (policy|period|schedule)|data retention|storage limitation)\b/i.test(corpus.text);
|
|
303
|
+
const consent = /\b(cookie consent|consent (banner|manager|mode)|cookiebot|usercentrics|klaro|borlabs)\b/i.test(corpus.text);
|
|
304
|
+
const erasure = /\b(right to erasure|data subject request|DSAR|DSR|account deletion|delete my data)\b/i.test(corpus.text);
|
|
305
|
+
const enforced = _ciBlocks(ci, 'privacy', 'gdpr', 'retention', 'consent', 'dsar');
|
|
306
|
+
const seen: string[] = [
|
|
307
|
+
...(notice ? ['a privacy notice or DPA'] : []),
|
|
308
|
+
...(retention ? ['a retention rule'] : []),
|
|
309
|
+
...(consent ? ['a consent mechanism'] : []),
|
|
310
|
+
...(erasure ? ['an erasure / data-subject-request path'] : []),
|
|
311
|
+
...(enforced ? ['a blocking CI step naming a privacy obligation'] : []),
|
|
312
|
+
];
|
|
313
|
+
return {
|
|
314
|
+
grade: _graded(notice || retention || consent || erasure, enforced),
|
|
315
|
+
evidence: seen.length > 0 ? seen.join('; ') : 'no privacy notice, retention rule, consent mechanism or erasure path found',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
152
319
|
export function grade(root: string): Matrix {
|
|
153
320
|
const ci = _ciText(root);
|
|
154
321
|
const hasCi = ci !== '';
|
|
@@ -310,6 +477,41 @@ export function grade(root: string): Matrix {
|
|
|
310
477
|
evidence: 'out of scope by design (see the roadmap exclusion table)',
|
|
311
478
|
});
|
|
312
479
|
|
|
480
|
+
// 11. operational recovery — NON-KNOCKOUT
|
|
481
|
+
//
|
|
482
|
+
// New dimensions are never knockouts. A knockout added today would re-bind
|
|
483
|
+
// every existing verdict at L0 on the day it shipped, which changes what the
|
|
484
|
+
// level MEANS while looking like a change to what it measures.
|
|
485
|
+
const corpus = _textCorpus(root);
|
|
486
|
+
const recovery = _recovery(corpus, ci);
|
|
487
|
+
dims.push({
|
|
488
|
+
id: 'operational-recovery',
|
|
489
|
+
label: 'operational recovery',
|
|
490
|
+
knockout: false,
|
|
491
|
+
grade: recovery.grade,
|
|
492
|
+
evidence: recovery.evidence,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 12. mail authenticity — NON-KNOCKOUT, own ladder (4.2)
|
|
496
|
+
const mail = _mailAuthenticity(corpus);
|
|
497
|
+
dims.push({
|
|
498
|
+
id: 'mail-authenticity',
|
|
499
|
+
label: 'mail authenticity',
|
|
500
|
+
knockout: false,
|
|
501
|
+
grade: mail.grade,
|
|
502
|
+
evidence: mail.evidence,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// 13. privacy obligations — NON-KNOCKOUT
|
|
506
|
+
const privacy = _privacyObligations(corpus, ci);
|
|
507
|
+
dims.push({
|
|
508
|
+
id: 'privacy-obligations',
|
|
509
|
+
label: 'privacy obligations',
|
|
510
|
+
knockout: false,
|
|
511
|
+
grade: privacy.grade,
|
|
512
|
+
evidence: privacy.evidence,
|
|
513
|
+
});
|
|
514
|
+
|
|
313
515
|
// ── the verdict: min over knockouts, undetectable binds at L0 ──────────
|
|
314
516
|
const knockouts = dims.filter((d) => d.knockout);
|
|
315
517
|
const undetectable = knockouts.find((d) => d.grade === null);
|
|
@@ -440,6 +642,23 @@ export function selfTest(): number {
|
|
|
440
642
|
return /%|\/100/.test(out) ? 1 : 0;
|
|
441
643
|
},
|
|
442
644
|
},
|
|
645
|
+
{
|
|
646
|
+
name: 'DMARC p=none scores strictly below p=quarantine — monitoring is not protection',
|
|
647
|
+
expect: 'accept',
|
|
648
|
+
run: () => {
|
|
649
|
+
const g = (zone: string): number => {
|
|
650
|
+
const d = grade(mk({
|
|
651
|
+
'dns/zone.txt': zone,
|
|
652
|
+
'dns/spf.txt': '@ IN TXT "v=spf1 include:_spf.example.net -all"\n',
|
|
653
|
+
'dns/dkim.txt': 'sel._domainkey IN TXT "v=DKIM1; k=rsa; p=MIIB"\n',
|
|
654
|
+
}));
|
|
655
|
+
return d.dimensions.find((x) => x.id === 'mail-authenticity')?.grade ?? -1;
|
|
656
|
+
};
|
|
657
|
+
const none = g('_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:d@example.com"\n');
|
|
658
|
+
const quarantine = g('_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:d@example.com"\n');
|
|
659
|
+
return none === 1 && quarantine === 2 ? 0 : 1;
|
|
660
|
+
},
|
|
661
|
+
},
|
|
443
662
|
{
|
|
444
663
|
name: 'rejects a missing target root rather than grading an empty tree',
|
|
445
664
|
expect: 'reject',
|