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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
5
- "packageVersion": "1.3.1108",
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": "d86f423e778835fbd6d7955c888a116f3a8e12233521a506cede29bac057c8f3",
2
+ "sha256": "3fbf817cb75d0316f22d4a85f6b5b3ab18b711c97ddb54efec9db095d31548c9",
3
3
  "registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
4
- "packageVersion": "1.3.1108"
4
+ "packageVersion": "1.3.1109"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
3
3
  "articleCount": 82,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1108"
5
+ "packageVersion": "1.3.1109"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1108",
3
+ "version": "1.3.1109",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- cadence: { reviewAfterDays, dueAreas, blocking: false },
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-01T21:22:23.622Z",
5
- "instarVersion": "1.3.1108",
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.1108",
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": "d86f423e778835fbd6d7955c888a116f3a8e12233521a506cede29bac057c8f3",
2
+ "sha256": "3fbf817cb75d0316f22d4a85f6b5b3ab18b711c97ddb54efec9db095d31548c9",
3
3
  "registrySha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
4
- "packageVersion": "1.3.1108"
4
+ "packageVersion": "1.3.1109"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "5413a0c6ef9ba2bda876b509d1c0bfebe450da3708cd5a5d627d6cb836012d58",
3
3
  "articleCount": 82,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1108"
5
+ "packageVersion": "1.3.1109"
6
6
  }
@@ -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}`.