instar 1.3.1108 → 1.3.1109
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/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/scripts/standards-coverage.mjs +327 -2
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1109.md +31 -0
- package/upgrades/eli16/standards-area-model-adequacy.md +33 -0
- package/upgrades/side-effects/standards-area-model-adequacy.md +192 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1109",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/canonical-migration-contracts.json",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "3fbf817cb75d0316f22d4a85f6b5b3ab18b711c97ddb54efec9db095d31548c9",
|
|
3
3
|
"registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1109"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* node scripts/standards-coverage.mjs --record-area-audit=all --audit-ref=docs/audits/review.json
|
|
35
35
|
* node scripts/standards-coverage.mjs --record-area-audit="The Root" --audit-ref=docs/audits/review.json
|
|
36
36
|
* node scripts/standards-coverage.mjs --record-area-audit=all --admit-new-areas --audit-ref=docs/audits/review.json
|
|
37
|
+
* node scripts/standards-coverage.mjs --record-area-model-audit --audit-ref=docs/audits/model-review.json
|
|
37
38
|
* node scripts/standards-coverage.mjs --allow-partial-registry # explicit non-CI partial checkout
|
|
38
39
|
*
|
|
39
40
|
* Floors (env override):
|
|
@@ -53,6 +54,7 @@ import crypto from 'node:crypto';
|
|
|
53
54
|
import { fileURLToPath } from 'node:url';
|
|
54
55
|
import yaml from 'js-yaml';
|
|
55
56
|
import { articleIds, parseRegistryStructure } from './standards-registry-article-core.mjs';
|
|
57
|
+
import { parseFrontmatter, validateAuditReport } from './write-audit-convergence.mjs';
|
|
56
58
|
|
|
57
59
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
58
60
|
const args = new Set(process.argv.slice(2));
|
|
@@ -63,6 +65,7 @@ const RECORD_AREA_ARG = [...args].find((arg) => arg.startsWith('--record-area-au
|
|
|
63
65
|
const RECORD_AREA = RECORD_AREA_ARG?.slice('--record-area-audit='.length) ?? null;
|
|
64
66
|
const AUDIT_REF_ARG = [...args].find((arg) => arg.startsWith('--audit-ref='));
|
|
65
67
|
const AUDIT_REF = AUDIT_REF_ARG?.slice('--audit-ref='.length) ?? null;
|
|
68
|
+
const RECORD_AREA_MODEL = args.has('--record-area-model-audit');
|
|
66
69
|
const ALLOW_PARTIAL_REGISTRY = args.has('--allow-partial-registry');
|
|
67
70
|
const ADMIT_NEW_AREAS = args.has('--admit-new-areas');
|
|
68
71
|
|
|
@@ -75,6 +78,7 @@ function resolveRoot() {
|
|
|
75
78
|
const ROOT = resolveRoot();
|
|
76
79
|
const REGISTRY_PATH = path.join(ROOT, 'docs', 'STANDARDS-REGISTRY.md');
|
|
77
80
|
const AREA_AUDITS_PATH = path.join(ROOT, 'docs', 'standards-registry-area-audits.json');
|
|
81
|
+
const AREA_MODEL_AUDIT_PATH = path.join(ROOT, 'docs', 'standards-registry-area-model-audit.json');
|
|
78
82
|
const CI_WORKFLOW_PATH = path.join(ROOT, '.github', 'workflows', 'ci.yml');
|
|
79
83
|
const OUT_PATH = path.join(ROOT, '.instar', 'standards-coverage.json');
|
|
80
84
|
|
|
@@ -336,6 +340,11 @@ function classifyFileGuard(ref) {
|
|
|
336
340
|
|
|
337
341
|
const AREA_AUDIT_SCHEMA_VERSION = 2;
|
|
338
342
|
const AUDIT_EVIDENCE_SCHEMA_VERSION = 1;
|
|
343
|
+
const AREA_MODEL_AUDIT_SCHEMA_VERSION = 1;
|
|
344
|
+
const AREA_MODEL_EVIDENCE_SCHEMA_VERSION = 1;
|
|
345
|
+
const AREA_MODEL_SCOPE = 'area-model-adequacy';
|
|
346
|
+
const AREA_MODEL_ACTIONS = ['keep', 'add', 'split', 'merge', 'retire'];
|
|
347
|
+
const CURRENT_AREA_DISPOSITIONS = new Set(['keep', 'split', 'merge', 'retire']);
|
|
339
348
|
const AREA_AUDIT_KEYS = [
|
|
340
349
|
'lastAuditedAt', 'auditRef', 'auditSha256', 'areaSha256', 'refResolutionFloor',
|
|
341
350
|
];
|
|
@@ -408,6 +417,10 @@ function textSha256(value) {
|
|
|
408
417
|
return crypto.createHash('sha256').update(canonicalText(value)).digest('hex');
|
|
409
418
|
}
|
|
410
419
|
|
|
420
|
+
function areaSetSha256(areaNames) {
|
|
421
|
+
return textSha256(`standards-area-model-v1\n${[...areaNames].sort().join('\n')}\n`);
|
|
422
|
+
}
|
|
423
|
+
|
|
411
424
|
function validateRootSelfWiring() {
|
|
412
425
|
const errors = [];
|
|
413
426
|
let stat;
|
|
@@ -618,6 +631,222 @@ function readAuditEvidence(ref) {
|
|
|
618
631
|
};
|
|
619
632
|
}
|
|
620
633
|
|
|
634
|
+
function canonicalAreaModelEvidence(value) {
|
|
635
|
+
const currentAreas = nullObject();
|
|
636
|
+
const sourceAreas = isPlainObject(value?.currentAreas) ? value.currentAreas : nullObject();
|
|
637
|
+
for (const area of Object.keys(sourceAreas).sort()) {
|
|
638
|
+
currentAreas[area] = {
|
|
639
|
+
disposition: sourceAreas[area]?.disposition,
|
|
640
|
+
rationale: sourceAreas[area]?.rationale,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const additions = Array.isArray(value?.additions)
|
|
644
|
+
? [...value.additions]
|
|
645
|
+
.map((entry) => ({ name: entry?.name, rationale: entry?.rationale }))
|
|
646
|
+
.sort((left, right) => String(left.name).localeCompare(String(right.name)))
|
|
647
|
+
: value?.additions;
|
|
648
|
+
return {
|
|
649
|
+
schemaVersion: value?.schemaVersion,
|
|
650
|
+
scope: value?.scope,
|
|
651
|
+
reviewedAt: value?.reviewedAt,
|
|
652
|
+
reviewers: value?.reviewers,
|
|
653
|
+
findingDisposition: value?.findingDisposition,
|
|
654
|
+
reviewedActions: value?.reviewedActions,
|
|
655
|
+
convergenceReport: value?.convergenceReport,
|
|
656
|
+
convergenceSha256: value?.convergenceSha256,
|
|
657
|
+
currentAreas,
|
|
658
|
+
additions,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function serializeAreaModelEvidence(value) {
|
|
663
|
+
return JSON.stringify(canonicalAreaModelEvidence(value), null, 2) + '\n';
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function readAreaModelEvidence(ref, expectedAreas) {
|
|
667
|
+
const resolved = resolveJailedRegularFile(ref, 'docs/audits', '.json');
|
|
668
|
+
const refError = typeof resolved === 'string' ? auditRefError(ref) : null;
|
|
669
|
+
if (refError) return { bytes: null, sha256: null, evidence: null, errors: [refError] };
|
|
670
|
+
let bytes;
|
|
671
|
+
try { bytes = fs.readFileSync(resolved.fullPath, 'utf-8'); } catch {
|
|
672
|
+
return { bytes: null, sha256: null, evidence: null, errors: ['cannot be read'] };
|
|
673
|
+
}
|
|
674
|
+
let evidence;
|
|
675
|
+
try { evidence = JSON.parse(bytes); } catch {
|
|
676
|
+
return { bytes, sha256: null, evidence: null, errors: ['is not valid JSON'] };
|
|
677
|
+
}
|
|
678
|
+
const errors = [];
|
|
679
|
+
if (canonicalText(bytes) !== serializeAreaModelEvidence(evidence)) errors.push('is not in canonical form');
|
|
680
|
+
const topKeys = isPlainObject(evidence) ? Object.keys(evidence).sort() : [];
|
|
681
|
+
const expectedTopKeys = [
|
|
682
|
+
'additions', 'convergenceReport', 'convergenceSha256', 'currentAreas',
|
|
683
|
+
'findingDisposition', 'reviewedActions', 'reviewedAt', 'reviewers',
|
|
684
|
+
'schemaVersion', 'scope',
|
|
685
|
+
];
|
|
686
|
+
if (!isPlainObject(evidence) || evidence.schemaVersion !== AREA_MODEL_EVIDENCE_SCHEMA_VERSION) {
|
|
687
|
+
errors.push(`must use schemaVersion ${AREA_MODEL_EVIDENCE_SCHEMA_VERSION}`);
|
|
688
|
+
}
|
|
689
|
+
if (JSON.stringify(topKeys) !== JSON.stringify(expectedTopKeys)) {
|
|
690
|
+
errors.push(`must contain exactly ${expectedTopKeys.join(', ')}`);
|
|
691
|
+
}
|
|
692
|
+
if (evidence?.scope !== AREA_MODEL_SCOPE) errors.push(`scope must be ${AREA_MODEL_SCOPE}`);
|
|
693
|
+
if (!canonicalTimestamp(evidence?.reviewedAt)) errors.push('has invalid reviewedAt');
|
|
694
|
+
if (!Array.isArray(evidence?.reviewers) || evidence.reviewers.length === 0 ||
|
|
695
|
+
evidence.reviewers.some((reviewer) => typeof reviewer !== 'string' ||
|
|
696
|
+
!/^[a-z0-9][a-z0-9:._/-]{1,119}$/i.test(reviewer)) ||
|
|
697
|
+
new Set(evidence.reviewers).size !== evidence.reviewers.length) {
|
|
698
|
+
errors.push('must name one or more unique reviewers');
|
|
699
|
+
}
|
|
700
|
+
const findingDisposition = evidence?.findingDisposition;
|
|
701
|
+
if (!isPlainObject(findingDisposition) ||
|
|
702
|
+
JSON.stringify(Object.keys(findingDisposition).sort()) !== JSON.stringify(['noUnresolvedDesign', 'resolvedFindings']) ||
|
|
703
|
+
findingDisposition.noUnresolvedDesign !== true ||
|
|
704
|
+
!Number.isSafeInteger(findingDisposition.resolvedFindings) || findingDisposition.resolvedFindings < 0) {
|
|
705
|
+
errors.push('must declare findingDisposition { noUnresolvedDesign: true, resolvedFindings: nonnegative integer }');
|
|
706
|
+
}
|
|
707
|
+
if (!Array.isArray(evidence?.reviewedActions) ||
|
|
708
|
+
JSON.stringify(evidence.reviewedActions) !== JSON.stringify(AREA_MODEL_ACTIONS)) {
|
|
709
|
+
errors.push(`reviewedActions must be exactly ${AREA_MODEL_ACTIONS.join(', ')}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const expected = [...expectedAreas].sort();
|
|
713
|
+
const actual = isPlainObject(evidence?.currentAreas) ? Object.keys(evidence.currentAreas).sort() : [];
|
|
714
|
+
if (!isPlainObject(evidence?.currentAreas)) errors.push('must contain a currentAreas object');
|
|
715
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
716
|
+
errors.push(`currentAreas must exactly cover the parsed family set (expected: ${expected.join(', ') || 'none'})`);
|
|
717
|
+
}
|
|
718
|
+
for (const area of actual) {
|
|
719
|
+
const entry = evidence.currentAreas[area];
|
|
720
|
+
if (!isPlainObject(entry) ||
|
|
721
|
+
JSON.stringify(Object.keys(entry).sort()) !== JSON.stringify(['disposition', 'rationale']) ||
|
|
722
|
+
!CURRENT_AREA_DISPOSITIONS.has(entry.disposition) ||
|
|
723
|
+
typeof entry.rationale !== 'string' || entry.rationale.trim().length < 24 || entry.rationale.length > 1000) {
|
|
724
|
+
errors.push(`currentAreas entry for ${area} must carry a keep/split/merge/retire disposition and a 24-1000 character rationale`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (!Array.isArray(evidence?.additions)) {
|
|
728
|
+
errors.push('additions must be an array (empty is the explicit no-add disposition)');
|
|
729
|
+
} else {
|
|
730
|
+
const seen = new Set();
|
|
731
|
+
for (const addition of evidence.additions) {
|
|
732
|
+
if (!isPlainObject(addition) ||
|
|
733
|
+
JSON.stringify(Object.keys(addition).sort()) !== JSON.stringify(['name', 'rationale']) ||
|
|
734
|
+
typeof addition.name !== 'string' || addition.name.trim() !== addition.name ||
|
|
735
|
+
addition.name.length < 2 || addition.name.length > 120 || seen.has(addition.name) ||
|
|
736
|
+
expected.includes(addition.name) ||
|
|
737
|
+
typeof addition.rationale !== 'string' || addition.rationale.trim().length < 24 || addition.rationale.length > 1000) {
|
|
738
|
+
errors.push('each additions entry must name one unique non-current area with a 24-1000 character rationale');
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
seen.add(addition.name);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const reportRef = evidence?.convergenceReport;
|
|
746
|
+
const resolvedReport = resolveJailedRegularFile(reportRef, 'docs/audits', '.md');
|
|
747
|
+
let reportBytes = null;
|
|
748
|
+
if (typeof resolvedReport === 'string') {
|
|
749
|
+
errors.push('must name an existing regular convergenceReport under docs/audits/');
|
|
750
|
+
} else {
|
|
751
|
+
try { reportBytes = fs.readFileSync(resolvedReport.fullPath, 'utf-8'); } catch {
|
|
752
|
+
errors.push('convergenceReport cannot be read');
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (typeof evidence?.convergenceSha256 !== 'string' || !SHA256_RE.test(evidence.convergenceSha256)) {
|
|
756
|
+
errors.push('has invalid convergenceSha256');
|
|
757
|
+
} else if (reportBytes !== null && textSha256(reportBytes) !== evidence.convergenceSha256) {
|
|
758
|
+
errors.push('convergenceReport bytes changed');
|
|
759
|
+
}
|
|
760
|
+
if (reportBytes !== null) {
|
|
761
|
+
const reportText = canonicalText(reportBytes);
|
|
762
|
+
let convergence;
|
|
763
|
+
try {
|
|
764
|
+
convergence = validateAuditReport(reportText, {
|
|
765
|
+
root: ROOT,
|
|
766
|
+
basenameSlug: path.basename(reportRef, '.md'),
|
|
767
|
+
requiredStandardsRef: 'docs/STANDARDS-REGISTRY.md',
|
|
768
|
+
standardEvidence: { responseChanged: false },
|
|
769
|
+
});
|
|
770
|
+
} catch (error) {
|
|
771
|
+
convergence = { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
772
|
+
}
|
|
773
|
+
if (!convergence.ok) {
|
|
774
|
+
errors.push(`convergenceReport has not earned convergence: ${convergence.reason}`);
|
|
775
|
+
} else {
|
|
776
|
+
const resolvedFindings = convergence.rounds.reduce((sum, round) => sum + round.rows.length, 0);
|
|
777
|
+
if (findingDisposition?.resolvedFindings !== resolvedFindings) {
|
|
778
|
+
errors.push(`findingDisposition resolvedFindings must equal the convergenceReport ledger count (${resolvedFindings})`);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const convergedAt = parseFrontmatter(reportText).fields.converged;
|
|
782
|
+
if (convergedAt !== evidence.reviewedAt) {
|
|
783
|
+
errors.push('reviewedAt must equal the convergenceReport earned timestamp');
|
|
784
|
+
}
|
|
785
|
+
} catch {
|
|
786
|
+
errors.push('convergenceReport frontmatter cannot be read');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return { bytes, sha256: textSha256(bytes), evidence, errors };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function canonicalAreaModelAuditRecord(value) {
|
|
794
|
+
return {
|
|
795
|
+
schemaVersion: value?.schemaVersion,
|
|
796
|
+
lastAuditedAt: value?.lastAuditedAt,
|
|
797
|
+
auditRef: value?.auditRef,
|
|
798
|
+
auditSha256: value?.auditSha256,
|
|
799
|
+
areaSetSha256: value?.areaSetSha256,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function serializeAreaModelAuditRecord(value) {
|
|
804
|
+
return JSON.stringify(canonicalAreaModelAuditRecord(value), null, 2) + '\n';
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function loadAreaModelAudit(expectedAreas) {
|
|
808
|
+
let raw;
|
|
809
|
+
try { raw = fs.readFileSync(AREA_MODEL_AUDIT_PATH, 'utf-8'); } catch {
|
|
810
|
+
return { record: null, errors: ['area model adequacy audit record is missing'] };
|
|
811
|
+
}
|
|
812
|
+
let record;
|
|
813
|
+
try { record = JSON.parse(raw); } catch {
|
|
814
|
+
return { record: null, errors: ['area model adequacy audit record is not valid JSON'] };
|
|
815
|
+
}
|
|
816
|
+
const errors = [];
|
|
817
|
+
const keys = isPlainObject(record) ? Object.keys(record).sort() : [];
|
|
818
|
+
const expectedKeys = ['areaSetSha256', 'auditRef', 'auditSha256', 'lastAuditedAt', 'schemaVersion'];
|
|
819
|
+
if (!isPlainObject(record) || record.schemaVersion !== AREA_MODEL_AUDIT_SCHEMA_VERSION ||
|
|
820
|
+
JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
|
|
821
|
+
errors.push(`area model adequacy audit record must use schemaVersion ${AREA_MODEL_AUDIT_SCHEMA_VERSION} and exact keys`);
|
|
822
|
+
}
|
|
823
|
+
if (!canonicalTimestamp(record?.lastAuditedAt)) {
|
|
824
|
+
errors.push('area model adequacy audit record has invalid lastAuditedAt');
|
|
825
|
+
} else if (Date.parse(record.lastAuditedAt) > Date.now() + 5 * 60_000) {
|
|
826
|
+
errors.push('area model adequacy audit record has a future lastAuditedAt');
|
|
827
|
+
}
|
|
828
|
+
const evidence = readAreaModelEvidence(record?.auditRef, expectedAreas);
|
|
829
|
+
for (const error of evidence.errors) errors.push(`area model adequacy auditRef ${error}`);
|
|
830
|
+
if (typeof record?.auditSha256 !== 'string' || !SHA256_RE.test(record.auditSha256)) {
|
|
831
|
+
errors.push('area model adequacy audit record has invalid auditSha256');
|
|
832
|
+
} else if (evidence.sha256 && record.auditSha256 !== evidence.sha256) {
|
|
833
|
+
errors.push('area model adequacy audit artifact changed');
|
|
834
|
+
}
|
|
835
|
+
const currentAreaSetSha256 = areaSetSha256(expectedAreas);
|
|
836
|
+
if (typeof record?.areaSetSha256 !== 'string' || !SHA256_RE.test(record.areaSetSha256)) {
|
|
837
|
+
errors.push('area model adequacy audit record has invalid areaSetSha256');
|
|
838
|
+
} else if (record.areaSetSha256 !== currentAreaSetSha256) {
|
|
839
|
+
errors.push('area model adequacy audit is stale for the current family set');
|
|
840
|
+
}
|
|
841
|
+
if (evidence.evidence?.reviewedAt !== record?.lastAuditedAt) {
|
|
842
|
+
errors.push('area model adequacy audit reviewedAt does not match lastAuditedAt');
|
|
843
|
+
}
|
|
844
|
+
if (isPlainObject(record) && canonicalText(raw) !== serializeAreaModelAuditRecord(record)) {
|
|
845
|
+
errors.push('area model adequacy audit record is not in canonical form');
|
|
846
|
+
}
|
|
847
|
+
return { record, evidence, currentAreaSetSha256, errors };
|
|
848
|
+
}
|
|
849
|
+
|
|
621
850
|
function loadAreaAuditLedger(expectedAreas) {
|
|
622
851
|
let raw;
|
|
623
852
|
try {
|
|
@@ -852,6 +1081,18 @@ function compute() {
|
|
|
852
1081
|
totalAreas: 0,
|
|
853
1082
|
errors: ALLOW_PARTIAL_REGISTRY ? [] : ['standards registry missing (use --allow-partial-registry only for a deliberate partial checkout)'],
|
|
854
1083
|
},
|
|
1084
|
+
areaModelAudit: {
|
|
1085
|
+
status: 'not-assessed',
|
|
1086
|
+
path: path.relative(ROOT, AREA_MODEL_AUDIT_PATH),
|
|
1087
|
+
schemaVersion: AREA_MODEL_AUDIT_SCHEMA_VERSION,
|
|
1088
|
+
currentAreaSetSha256: null,
|
|
1089
|
+
auditedAreaSetSha256: null,
|
|
1090
|
+
auditCurrent: false,
|
|
1091
|
+
lastAuditedAt: null,
|
|
1092
|
+
auditRef: null,
|
|
1093
|
+
errors: [],
|
|
1094
|
+
},
|
|
1095
|
+
cadence: { reviewAfterDays: 90, dueAreas: [], areaModelReviewDue: false, blocking: false },
|
|
855
1096
|
enforcementScope: {
|
|
856
1097
|
recognizedHeadings: [...ENFORCEMENT_SECTION_HEADINGS],
|
|
857
1098
|
excludedProvenanceHeadings: [...EXCLUDED_PROVENANCE_SECTION_HEADINGS],
|
|
@@ -922,6 +1163,7 @@ function compute() {
|
|
|
922
1163
|
const enforcedRatio = total === 0 ? 1 : Number((enforced / total).toFixed(4));
|
|
923
1164
|
const areaNames = [...areaTallies.keys()].sort();
|
|
924
1165
|
const loadedAreaAudits = loadAreaAuditLedger(areaNames);
|
|
1166
|
+
const loadedAreaModelAudit = loadAreaModelAudit(areaNames);
|
|
925
1167
|
const baseComparison = compareLedgerToBase(loadedAreaAudits.ledger);
|
|
926
1168
|
const rootSelfWiring = ALLOW_PARTIAL_REGISTRY
|
|
927
1169
|
? { status: 'not-assessed', errors: [] }
|
|
@@ -975,6 +1217,13 @@ function compute() {
|
|
|
975
1217
|
return !Number.isFinite(audited) || nowMs - audited >= reviewAfterDays * 86_400_000;
|
|
976
1218
|
})
|
|
977
1219
|
.map(([area]) => area);
|
|
1220
|
+
const modelLastAuditedMs = Date.parse(loadedAreaModelAudit.record?.lastAuditedAt ?? '');
|
|
1221
|
+
const areaModelReviewDue = !Number.isFinite(modelLastAuditedMs) ||
|
|
1222
|
+
nowMs - modelLastAuditedMs >= reviewAfterDays * 86_400_000;
|
|
1223
|
+
const modelRecord = loadedAreaModelAudit.record;
|
|
1224
|
+
const currentAreaSetSha256 = loadedAreaModelAudit.currentAreaSetSha256 ?? areaSetSha256(areaNames);
|
|
1225
|
+
const modelAuditCurrent = loadedAreaModelAudit.errors.length === 0 &&
|
|
1226
|
+
modelRecord?.areaSetSha256 === currentAreaSetSha256;
|
|
978
1227
|
return {
|
|
979
1228
|
generatedAt: new Date().toISOString(),
|
|
980
1229
|
registryFound: true,
|
|
@@ -989,7 +1238,18 @@ function compute() {
|
|
|
989
1238
|
protectedBaseStatus: baseComparison.status,
|
|
990
1239
|
errors: areaAuditErrors,
|
|
991
1240
|
},
|
|
992
|
-
|
|
1241
|
+
areaModelAudit: {
|
|
1242
|
+
status: loadedAreaModelAudit.errors.length === 0 ? 'current' : 'invalid',
|
|
1243
|
+
path: path.relative(ROOT, AREA_MODEL_AUDIT_PATH),
|
|
1244
|
+
schemaVersion: AREA_MODEL_AUDIT_SCHEMA_VERSION,
|
|
1245
|
+
currentAreaSetSha256,
|
|
1246
|
+
auditedAreaSetSha256: typeof modelRecord?.areaSetSha256 === 'string' ? modelRecord.areaSetSha256 : null,
|
|
1247
|
+
auditCurrent: modelAuditCurrent,
|
|
1248
|
+
lastAuditedAt: typeof modelRecord?.lastAuditedAt === 'string' ? modelRecord.lastAuditedAt : null,
|
|
1249
|
+
auditRef: typeof modelRecord?.auditRef === 'string' ? modelRecord.auditRef : null,
|
|
1250
|
+
errors: loadedAreaModelAudit.errors,
|
|
1251
|
+
},
|
|
1252
|
+
cadence: { reviewAfterDays, dueAreas, areaModelReviewDue, blocking: false },
|
|
993
1253
|
falseClaimCount: falseClaims.length, falseClaims,
|
|
994
1254
|
danglingCount, danglingByStandard,
|
|
995
1255
|
};
|
|
@@ -1102,6 +1362,58 @@ function recordAreaAudit(report, selection, auditRef) {
|
|
|
1102
1362
|
fs.renameSync(tmp, AREA_AUDITS_PATH);
|
|
1103
1363
|
}
|
|
1104
1364
|
|
|
1365
|
+
function readAreaModelAuditForRecord() {
|
|
1366
|
+
if (!fs.existsSync(AREA_MODEL_AUDIT_PATH)) return null;
|
|
1367
|
+
let raw;
|
|
1368
|
+
let record;
|
|
1369
|
+
try {
|
|
1370
|
+
raw = fs.readFileSync(AREA_MODEL_AUDIT_PATH, 'utf-8');
|
|
1371
|
+
record = JSON.parse(raw);
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
throw new Error(`refusing to overwrite an unreadable area model audit record: ${error instanceof Error ? error.message : String(error)}`);
|
|
1374
|
+
}
|
|
1375
|
+
const keys = isPlainObject(record) ? Object.keys(record).sort() : [];
|
|
1376
|
+
const expectedKeys = ['areaSetSha256', 'auditRef', 'auditSha256', 'lastAuditedAt', 'schemaVersion'];
|
|
1377
|
+
if (!isPlainObject(record) || record.schemaVersion !== AREA_MODEL_AUDIT_SCHEMA_VERSION ||
|
|
1378
|
+
JSON.stringify(keys) !== JSON.stringify(expectedKeys) ||
|
|
1379
|
+
!canonicalTimestamp(record.lastAuditedAt) ||
|
|
1380
|
+
typeof record.auditRef !== 'string' ||
|
|
1381
|
+
typeof record.auditSha256 !== 'string' || !SHA256_RE.test(record.auditSha256) ||
|
|
1382
|
+
typeof record.areaSetSha256 !== 'string' || !SHA256_RE.test(record.areaSetSha256) ||
|
|
1383
|
+
canonicalText(raw) !== serializeAreaModelAuditRecord(record)) {
|
|
1384
|
+
throw new Error('refusing to overwrite an invalid area model audit record');
|
|
1385
|
+
}
|
|
1386
|
+
return record;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function recordAreaModelAudit(report, auditRef) {
|
|
1390
|
+
if (!report.registryFound) throw new Error('cannot record an area model audit because the standards registry is absent');
|
|
1391
|
+
const currentAreas = Object.keys(report.areas).sort();
|
|
1392
|
+
const evidence = readAreaModelEvidence(auditRef, currentAreas);
|
|
1393
|
+
if (evidence.errors.length > 0) {
|
|
1394
|
+
throw new Error(`--audit-ref areaModelReview ${evidence.errors.join('; ')}`);
|
|
1395
|
+
}
|
|
1396
|
+
const prior = readAreaModelAuditForRecord();
|
|
1397
|
+
const lastAuditedAt = evidence.evidence.reviewedAt;
|
|
1398
|
+
if (Date.parse(lastAuditedAt) > Date.now() + 5 * 60_000) {
|
|
1399
|
+
throw new Error('area model lastAuditedAt may not be more than five minutes in the future');
|
|
1400
|
+
}
|
|
1401
|
+
if (prior && Date.parse(lastAuditedAt) < Date.parse(prior.lastAuditedAt)) {
|
|
1402
|
+
throw new Error('area model lastAuditedAt may not move backward');
|
|
1403
|
+
}
|
|
1404
|
+
const next = {
|
|
1405
|
+
schemaVersion: AREA_MODEL_AUDIT_SCHEMA_VERSION,
|
|
1406
|
+
lastAuditedAt,
|
|
1407
|
+
auditRef,
|
|
1408
|
+
auditSha256: evidence.sha256,
|
|
1409
|
+
areaSetSha256: areaSetSha256(currentAreas),
|
|
1410
|
+
};
|
|
1411
|
+
const tmp = `${AREA_MODEL_AUDIT_PATH}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
1412
|
+
fs.mkdirSync(path.dirname(AREA_MODEL_AUDIT_PATH), { recursive: true });
|
|
1413
|
+
fs.writeFileSync(tmp, serializeAreaModelAuditRecord(next), { flag: 'wx' });
|
|
1414
|
+
fs.renameSync(tmp, AREA_MODEL_AUDIT_PATH);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1105
1417
|
function main() {
|
|
1106
1418
|
let report = compute();
|
|
1107
1419
|
if (RECORD_AREA_ARG !== undefined) {
|
|
@@ -1113,6 +1425,15 @@ function main() {
|
|
|
1113
1425
|
process.exit(1);
|
|
1114
1426
|
}
|
|
1115
1427
|
}
|
|
1428
|
+
if (RECORD_AREA_MODEL) {
|
|
1429
|
+
try {
|
|
1430
|
+
recordAreaModelAudit(report, AUDIT_REF);
|
|
1431
|
+
report = compute();
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
process.stderr.write(`❌ standards-coverage area-model record failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1116
1437
|
report.floors = {
|
|
1117
1438
|
...FLOORS,
|
|
1118
1439
|
byArea: Object.fromEntries(
|
|
@@ -1152,6 +1473,9 @@ function main() {
|
|
|
1152
1473
|
for (const error of report.areaAudit.errors) {
|
|
1153
1474
|
console.error(`[standards-coverage] AREA AUDIT — ${error}`);
|
|
1154
1475
|
}
|
|
1476
|
+
for (const error of report.areaModelAudit.errors) {
|
|
1477
|
+
console.error(`[standards-coverage] AREA MODEL AUDIT — ${error}`);
|
|
1478
|
+
}
|
|
1155
1479
|
for (const fc of report.falseClaims) {
|
|
1156
1480
|
console.error(`[standards-coverage] FALSE CLAIM — "${fc.standard}" asserts running machinery (${fc.claims.map((c) => `"${c}"`).join(', ')}) but names no resolvable guard.`);
|
|
1157
1481
|
}
|
|
@@ -1164,6 +1488,7 @@ function main() {
|
|
|
1164
1488
|
failures.push(`enforced ratio ${aggregateEnforced}/${report.total} (${report.enforcedRatio}) < floor ${FLOORS.enforcedRatio}`);
|
|
1165
1489
|
}
|
|
1166
1490
|
for (const error of report.areaAudit.errors) failures.push(error);
|
|
1491
|
+
for (const error of report.areaModelAudit.errors) failures.push(error);
|
|
1167
1492
|
for (const [area, measurement] of Object.entries(report.areas)) {
|
|
1168
1493
|
if (ratioBelowFloor(measurement.enforced, measurement.total, measurement.refResolutionFloor)) {
|
|
1169
1494
|
failures.push(
|
|
@@ -1191,7 +1516,7 @@ function main() {
|
|
|
1191
1516
|
if (failures.length > 0) {
|
|
1192
1517
|
process.stderr.write('\n❌ standards-coverage check failed:\n');
|
|
1193
1518
|
for (const f of failures) process.stderr.write(` - ${f}\n`);
|
|
1194
|
-
process.stderr.write('\nFix: build a guard for an unguarded standard, record the changed family audit without lowering its floor, repair a dangling reference, classify each unknown article heading, or resolve a false claim whose prose asserts unnamed machinery.\n');
|
|
1519
|
+
process.stderr.write('\nFix: build a guard for an unguarded standard, record the changed family audit without lowering its floor, record a converged area-model adequacy review, repair a dangling reference, classify each unknown article heading, or resolve a false claim whose prose asserts unnamed machinery.\n');
|
|
1195
1520
|
process.exit(1);
|
|
1196
1521
|
}
|
|
1197
1522
|
if (!QUIET) console.error('✅ standards-coverage check passed.');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-08-02T03:42:37.445Z",
|
|
5
|
+
"instarVersion": "1.3.1109",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1109",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/canonical-migration-contracts.json",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "3fbf817cb75d0316f22d4a85f6b5b3ab18b711c97ddb54efec9db095d31548c9",
|
|
3
3
|
"registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1109"
|
|
5
5
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The Standards Registry audit now treats the fundamental-area list as a living
|
|
9
|
+
model. A separate convergence-bound record requires explicit keep, add, split,
|
|
10
|
+
merge, and retire consideration, while the weekly cadence resurfaces model
|
|
11
|
+
adequacy independently from per-family content age. The deterministic checker
|
|
12
|
+
validates evidence and exact family-set integrity but does not make semantic
|
|
13
|
+
taxonomy decisions.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
None — internal change (no user-facing surface).
|
|
18
|
+
|
|
19
|
+
## Summary of New Capabilities
|
|
20
|
+
|
|
21
|
+
None — internal change (no user-facing surface).
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
- A fail-before proof showed the prior checker accepted a missing model review
|
|
26
|
+
and could not distinguish family-content evidence from area-model evidence.
|
|
27
|
+
- The focused standards-coverage suite passes all 33 cases, including missing
|
|
28
|
+
evidence, wrong evidence kind, exact family-set binding, and live-repository
|
|
29
|
+
coverage.
|
|
30
|
+
- The initial whole-corpus review earned convergence after two rounds with all
|
|
31
|
+
six current families kept and no additions, splits, merges, or retirements.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Fundamental areas are now reviewed as a living model
|
|
2
|
+
|
|
3
|
+
Instar's Standards Registry is organized into six families. The existing audit
|
|
4
|
+
mechanism already did an important job: it remembered the exact contents of each
|
|
5
|
+
family, noticed when those contents changed, and prevented one large family's
|
|
6
|
+
coverage score from hiding a regression in a smaller family.
|
|
7
|
+
|
|
8
|
+
That still left a level above the contents unaudited. A perfectly preserved list
|
|
9
|
+
can be the wrong list. For example, two families might gradually overlap, one
|
|
10
|
+
might become too broad and need splitting, or a new cluster of standards might
|
|
11
|
+
deserve its own family. Checking that the six familiar names are unchanged does
|
|
12
|
+
not answer any of those questions.
|
|
13
|
+
|
|
14
|
+
This change adds a separate area-model adequacy review. A reviewer must examine
|
|
15
|
+
the whole family decomposition to convergence and record an explicit decision
|
|
16
|
+
for every current family: keep, split, merge, or retire. The review must also
|
|
17
|
+
state whether a new family should be added. The evidence is bound to the exact
|
|
18
|
+
convergence report and the exact current family set, so an ordinary content
|
|
19
|
+
review cannot be reused as proof that the model itself was reconsidered.
|
|
20
|
+
|
|
21
|
+
The first real review used two independent search angles over all 82 standards.
|
|
22
|
+
It kept all six current families and found no family to add, split, merge, or
|
|
23
|
+
retire. That result is intentionally described as a current judgment, not a
|
|
24
|
+
permanent taxonomy.
|
|
25
|
+
|
|
26
|
+
The weekly cadence now resurfaces this model-level review after 90 days alongside
|
|
27
|
+
the existing family-content reminders. Age remains a reminder rather than a CI
|
|
28
|
+
veto. CI does block when the required evidence is missing, malformed, detached
|
|
29
|
+
from its converged report, or stale because the family set changed. The checker
|
|
30
|
+
only verifies those closed structural facts; it never chooses the semantic
|
|
31
|
+
dispositions. This preserves the right split: deterministic machinery makes the
|
|
32
|
+
review impossible to forget, while a reasoning reviewer remains responsible for
|
|
33
|
+
deciding whether the fundamental areas are still adequate.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Side-Effects Review — Standards area-model adequacy audit
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `standards-area-model-adequacy`
|
|
4
|
+
**Date:** `2026-08-01`
|
|
5
|
+
**Author:** `instar-codey`
|
|
6
|
+
**Second-pass reviewer:** `not required — repository-only governance data and CI structure; no runtime, messaging, session, coherence, or self-triggered controller path`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
This change adds a separate evidence lifecycle for periodically reconsidering
|
|
11
|
+
whether the Standards Registry's family list is still adequate. The coverage
|
|
12
|
+
script now validates a canonical model-review artifact, its earned convergence
|
|
13
|
+
report, explicit keep/add/split/merge/retire consideration, the exact parsed
|
|
14
|
+
family set, byte hashes, and timestamps. The weekly workflow resurfaces this
|
|
15
|
+
review independently from family-content reviews. The initial two-round audit,
|
|
16
|
+
record, tests, ELI16, and internal-only release fragment ship atomically. No
|
|
17
|
+
runtime `src` file changes.
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
- **Area-model evidence validity** — add — a closed repository invariant blocks
|
|
22
|
+
CI when the record is missing, malformed, byte-detached, unconverged, or stale
|
|
23
|
+
for the parsed family set.
|
|
24
|
+
- **Semantic area disposition** — pass-through — reviewers choose keep, add,
|
|
25
|
+
split, merge, and retire outcomes; deterministic code never chooses them.
|
|
26
|
+
- **Review-age cadence** — modify — a 90-day model-review due signal joins the
|
|
27
|
+
existing content-review signal without gaining blocking authority.
|
|
28
|
+
- **Scheduled issue lifecycle** — modify — the existing single marker-owned
|
|
29
|
+
GitHub issue includes whichever content or model review is due.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Over-block
|
|
34
|
+
|
|
35
|
+
A legitimate registry change that adds, renames, or removes a family now fails
|
|
36
|
+
until a convergence-bound area-model review explicitly covers the new exact set.
|
|
37
|
+
That is intentional: the former behavior let a taxonomy change land with only a
|
|
38
|
+
content attestation. Formatting or wording changes inside a convergence report
|
|
39
|
+
also stale its evidence hash and require re-recording. Ordinary per-family
|
|
40
|
+
content evidence is deliberately rejected as model evidence even when its date
|
|
41
|
+
and reviewers are valid.
|
|
42
|
+
|
|
43
|
+
Elapsed age does not block. A repository with an unchanged but old review stays
|
|
44
|
+
green and receives the existing advisory issue instead, avoiding a calendar wall
|
|
45
|
+
that could veto unrelated work.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Under-block
|
|
50
|
+
|
|
51
|
+
The checker cannot prove that a rationale is intellectually good or that a
|
|
52
|
+
reviewer explored every meaningful alternative. It proves that all current
|
|
53
|
+
families have one allowed disposition, additions are explicit, all five actions
|
|
54
|
+
were reviewed, the evidence matches an earned convergence report, and no design
|
|
55
|
+
finding remains open. Semantic adequacy remains reasoning authority in the
|
|
56
|
+
review report.
|
|
57
|
+
|
|
58
|
+
The 90-day interval is a resurfacing cadence, not proof that the model becomes
|
|
59
|
+
wrong on day 91. Conversely, a poor but structurally valid no-change judgment can
|
|
60
|
+
pass; that is the irreducible judgment boundary and is made visible rather than
|
|
61
|
+
silently delegated to code.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 3. Level-of-abstraction fit
|
|
66
|
+
|
|
67
|
+
The change extends the existing standards-coverage script and existing scheduled
|
|
68
|
+
issue instead of inventing a parallel taxonomy service or runtime controller.
|
|
69
|
+
Per-family byte review and family-model adequacy remain separate evidence kinds
|
|
70
|
+
because they answer different questions and must not substitute for one another.
|
|
71
|
+
The shared convergence validator owns the review-process proof; the coverage
|
|
72
|
+
script only binds that proof to the exact family set and declared dispositions.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 4. Signal vs authority compliance
|
|
77
|
+
|
|
78
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
79
|
+
|
|
80
|
+
- [x] No brittle semantic detector holds blocking authority.
|
|
81
|
+
|
|
82
|
+
CI authority is limited to closed, enumerable facts: exact keys, allowed enums,
|
|
83
|
+
canonical timestamps, jailed regular-file references, hashes, earned convergence,
|
|
84
|
+
ledger count agreement, and exact family-set equality. The fallible question —
|
|
85
|
+
whether an area should actually be kept, added, split, merged, or retired — is
|
|
86
|
+
not scored or selected by the checker. Review age remains a signal consumed by
|
|
87
|
+
the existing advisory issue lifecycle.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 4b. Judgment-point check
|
|
92
|
+
|
|
93
|
+
No static heuristic is added at a competing-signals decision point. Structural
|
|
94
|
+
evidence validity is an enumerable invariant. Area-model adequacy is explicitly a
|
|
95
|
+
judgment point, and the mechanism preserves it as reviewer authority rather than
|
|
96
|
+
encoding proxy rules such as family size or keyword counts.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 5. Interactions
|
|
101
|
+
|
|
102
|
+
- **Shadowing:** model errors are additive to aggregate coverage, per-family
|
|
103
|
+
floors, content freshness, dangling references, unknown headings, and false
|
|
104
|
+
claims; none suppresses reporting of another.
|
|
105
|
+
- **Double-fire:** the same marker-owned cadence issue carries both content and
|
|
106
|
+
model due states, so simultaneous due dates update one issue rather than open
|
|
107
|
+
competing notices.
|
|
108
|
+
- **Races:** normal report/check modes are read-only. Record mode writes the
|
|
109
|
+
single model record through a unique temporary file and atomic rename.
|
|
110
|
+
- **Feedback loops:** the due issue never changes registry or evidence state and
|
|
111
|
+
therefore cannot make its own condition green.
|
|
112
|
+
- **Evidence separation:** a content-review artifact fails the model schema, so
|
|
113
|
+
existing family audits cannot accidentally satisfy the new obligation.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 6. External surfaces
|
|
118
|
+
|
|
119
|
+
GitHub Actions gains additional summary and issue text inside the existing
|
|
120
|
+
weekly standards cadence. No new issue class, API, credential, user message,
|
|
121
|
+
dashboard action, runtime configuration, database, or agent-home state is added.
|
|
122
|
+
The model record and evidence are committed repository data. There are no
|
|
123
|
+
operator-facing actions; the review is performed through the normal repository
|
|
124
|
+
change surface.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 6b. Operator-surface quality
|
|
129
|
+
|
|
130
|
+
No operator surface — not applicable.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 7. Multi-machine posture
|
|
135
|
+
|
|
136
|
+
**Replicated through Git.** The checker, family list, convergence report,
|
|
137
|
+
evidence, and canonical record ship in one commit. Every machine on the same
|
|
138
|
+
commit derives the same family-set and byte hashes. The scheduled notice is
|
|
139
|
+
repository-scoped GitHub state and already has one marker-owned voice. The
|
|
140
|
+
change emits no user-facing notices, holds no topic-scoped durable state, and
|
|
141
|
+
generates no URLs.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 8. Rollback cost
|
|
146
|
+
|
|
147
|
+
Rollback is one repository revert: remove the script/workflow/test changes and
|
|
148
|
+
the model record/evidence/report. Older code ignores the new JSON files, so no
|
|
149
|
+
data migration or agent state repair is needed. If the cadence issue is open,
|
|
150
|
+
the older workflow will update or close it according to content-review state.
|
|
151
|
+
There is no runtime propagation window because no shipped runtime code changes.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Conclusion
|
|
156
|
+
|
|
157
|
+
The review preserves the decisive boundary: structure makes area-model review
|
|
158
|
+
unforgettable and non-substitutable, while semantic taxonomy remains a converged
|
|
159
|
+
human/agent judgment. The change is isolated to repository governance, composes
|
|
160
|
+
with the prior area ratchet, has a clean Git rollback, and is clear to ship after
|
|
161
|
+
the focused and repository-wide checks remain green.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Second-pass review
|
|
166
|
+
|
|
167
|
+
**Reviewer:** not required
|
|
168
|
+
**Independent read of the artifact:** not required
|
|
169
|
+
|
|
170
|
+
No runtime controller or block/allow path over messages, sessions, dispatch,
|
|
171
|
+
coherence, trust, or recovery changes. The only veto is closed repository
|
|
172
|
+
evidence integrity, and semantic choices remain outside deterministic authority.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Evidence pointers
|
|
177
|
+
|
|
178
|
+
- `docs/audits/standards-area-model-adequacy.md`
|
|
179
|
+
- `docs/audits/standards-area-model-audit-2026-08-01.json`
|
|
180
|
+
- `docs/standards-registry-area-model-audit.json`
|
|
181
|
+
- `tests/unit/standards-coverage-ratchet.test.ts`
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Class-Closure Declaration (display-only mirror)
|
|
186
|
+
|
|
187
|
+
`defectClass: claim-vs-evidence`, `closure: guard`, `guardEvidence:
|
|
188
|
+
{enforcementType: ratchet, citation:
|
|
189
|
+
tests/unit/standards-coverage-ratchet.test.ts, howCaught: removing the area-model
|
|
190
|
+
record fails the live check, and presenting content-only evidence to the model
|
|
191
|
+
record path is rejected, so exact family-content proof can no longer masquerade
|
|
192
|
+
as proof that the area model itself was reconsidered}`.
|