@ankhorage/devtools 1.10.13 → 1.10.14

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.
@@ -34,7 +34,7 @@ once.
34
34
  From the target repository, run:
35
35
 
36
36
  ```text
37
- bun .agents/skills/zora-designer/scripts/owner-api.mjs inspect
37
+ bun .agents/skills/zora-designer/scripts/owner-api.ts inspect
38
38
  ```
39
39
 
40
40
  Use its installed owner output for categories, recommendations, harmonies, tone pairs, navigation
@@ -76,7 +76,7 @@ under `assets/images/`; rebuild text, controls, icons, surfaces, and layout with
76
76
  Scaffold only a reviewed, release-valid manifest:
77
77
 
78
78
  ```text
79
- bun .agents/skills/zora-designer/scripts/scaffold-template.mjs scaffold-input.json
79
+ bun .agents/skills/zora-designer/scripts/scaffold-template.ts scaffold-input.json
80
80
  ```
81
81
 
82
82
  The helper creates the template directory and regenerates discovery from the filesystem. Do not add
@@ -2,7 +2,7 @@
2
2
 
3
3
  `zora-designer.md` is the deterministic design/audit decision record. It is never runtime theme or
4
4
  manifest authority. The serializer and its ordered frontmatter/section skeleton live in
5
- [audit.mjs](../scripts/audit.mjs); do not maintain another template.
5
+ [audit.ts](../scripts/audit.ts); do not maintain another template.
6
6
 
7
7
  ## Document lifecycle
8
8
 
@@ -2,13 +2,13 @@
2
2
 
3
3
  The canonical rubric, weights, criterion keys, status factors, confidence factors, and release-gate
4
4
  inventory live in [audit-rubric.json](../assets/audit-rubric.json). The calculation implementation
5
- in [audit.mjs](../scripts/audit.mjs) loads that file. Do not reproduce either list or the arithmetic
5
+ in [audit.ts](../scripts/audit.ts) loads that file. Do not reproduce either list or the arithmetic
6
6
  in prompts, references, tests, or application code.
7
7
 
8
8
  Inspect the current catalog when needed:
9
9
 
10
10
  ```text
11
- bun .agents/skills/zora-designer/scripts/audit.mjs catalog
11
+ bun .agents/skills/zora-designer/scripts/audit.ts catalog
12
12
  ```
13
13
 
14
14
  ## Evidence intake
@@ -56,7 +56,7 @@ model to improvise:
56
56
  Inspect the skill-owned design rules before rendering:
57
57
 
58
58
  ```text
59
- bun .agents/skills/zora-designer/scripts/audit.mjs catalog
59
+ bun .agents/skills/zora-designer/scripts/audit.ts catalog
60
60
  ```
61
61
 
62
62
  Apply every relevant `generation` rule from that catalog. The rubric belongs to this skill; owner
@@ -10,7 +10,7 @@ Read repository instructions, an existing manifest or `zora-designer.md`, instal
10
10
  configuration, and relevant screens. Run:
11
11
 
12
12
  ```text
13
- bun .agents/skills/zora-designer/scripts/owner-api.mjs inspect
13
+ bun .agents/skills/zora-designer/scripts/owner-api.ts inspect
14
14
  ```
15
15
 
16
16
  The output is the choice source. Do not use remembered categories, color options, tone pairs,
@@ -58,7 +58,7 @@ screen brief. Changing the screen list invalidates navigator confirmation.
58
58
  Provide the resolved `category`, theme overrides, navigator, screens, and region decisions to:
59
59
 
60
60
  ```text
61
- bun .agents/skills/zora-designer/scripts/owner-api.mjs compose design-input.json
61
+ bun .agents/skills/zora-designer/scripts/owner-api.ts compose design-input.json
62
62
  ```
63
63
 
64
64
  Use owner-returned theme configuration, generated roles, computed Surface themes, diagnostics,
@@ -21,9 +21,112 @@ const HUMAN_SECTIONS = [
21
21
  'User notes',
22
22
  ];
23
23
 
24
+ interface AuditRubric {
25
+ confidenceFactors: Record<string, number>;
26
+ releaseGates: ReleaseGateDefinition[];
27
+ rules: RuleDefinition[];
28
+ statusFactors: Record<string, number | null>;
29
+ }
30
+
31
+ interface RuleDefinition {
32
+ criteria: string[];
33
+ id: string;
34
+ name: string;
35
+ weight: number;
36
+ }
37
+
38
+ interface ReleaseGateDefinition {
39
+ criteria: string[];
40
+ id: string;
41
+ name: string;
42
+ }
43
+
44
+ interface EvidenceItem extends Record<string, unknown> {
45
+ confidence: string;
46
+ confidenceFactor: number;
47
+ evidenceLevel: string;
48
+ id: string;
49
+ kind: string;
50
+ limitations: unknown[];
51
+ location: string;
52
+ observation: string;
53
+ reproduction: string;
54
+ }
55
+
56
+ interface CriterionResult {
57
+ applicable: boolean;
58
+ confidenceFactor: number | null;
59
+ essentialEvidenceIds: string[];
60
+ evidenceIds: string[];
61
+ id: string;
62
+ reason: string;
63
+ status: string;
64
+ statusFactor: number | null;
65
+ }
66
+
67
+ interface RuleResult {
68
+ applicableCount: number;
69
+ assessedCount: number;
70
+ assessedWeight: number;
71
+ assessmentFraction: number | null;
72
+ confidence: { label: string | null; value: number | null };
73
+ criteria: CriterionResult[];
74
+ earnedWeight: number;
75
+ name: string;
76
+ rule: string;
77
+ status: string;
78
+ statusFactor: number | null;
79
+ weight: number;
80
+ }
81
+
82
+ interface ReleaseGateResult {
83
+ applicable: boolean;
84
+ criteria: {
85
+ applicable: boolean;
86
+ evidenceIds: string[];
87
+ id: string;
88
+ reason: string;
89
+ status: string;
90
+ }[];
91
+ evidenceIds: string[];
92
+ id: string;
93
+ name: string;
94
+ reason: string;
95
+ status: string;
96
+ }
97
+
98
+ interface Finding extends Record<string, unknown> {
99
+ confidence: string;
100
+ criterionId: string;
101
+ evidence: string;
102
+ evidenceIds: string[];
103
+ evidenceLevel: string;
104
+ expected: string;
105
+ fix: string;
106
+ id: string;
107
+ impact: string;
108
+ location: string;
109
+ relatedRules: unknown[];
110
+ rootCause: string;
111
+ rule: string;
112
+ severity: string;
113
+ verification: string;
114
+ }
115
+
116
+ interface AuditTotals {
117
+ applicableWeight: number;
118
+ assessedWeight: number;
119
+ confidence: number | null;
120
+ earnedWeight: number;
121
+ possibleRange: { lower: number | null; upper: number | null };
122
+ score: number | null;
123
+ coverage: number | null;
124
+ }
125
+
24
126
  /*** Load the single canonical audit rubric and verify its invariant total weight. */
25
- export async function loadAuditRubric() {
26
- const rubric = JSON.parse(await readFile(RUBRIC_URL, 'utf8'));
127
+ export async function loadAuditRubric(): Promise<AuditRubric> {
128
+ const rubric: unknown = JSON.parse(await readFile(RUBRIC_URL, 'utf8'));
129
+ assertAuditRubric(rubric);
27
130
  const totalWeight = rubric.rules.reduce((sum, rule) => sum + rule.weight, 0);
28
131
  if (totalWeight !== 100) {
29
132
  throw new Error(`Canonical zora-designer rubric weight must equal 100; found ${totalWeight}.`);
@@ -32,7 +135,7 @@ export async function loadAuditRubric() {
32
135
  }
33
136
 
34
137
  /*** Calculate criterion, rule, coverage, confidence, score range, finding impact, and release gates. */
35
- export async function calculateAudit(input) {
138
+ export async function calculateAudit(input: unknown) {
36
139
  assertRecord(input, 'Audit input');
37
140
  const rubric = await loadAuditRubric();
38
141
  const evidence = Array.isArray(input.evidence) ? input.evidence : [];
@@ -42,8 +145,9 @@ export async function calculateAudit(input) {
42
145
  calculateRule(rule, assessments[rule.id], evidenceById, rubric.statusFactors),
43
146
  );
44
147
  const totals = calculateTotals(ruleResults);
148
+ const releaseGateInputs = isRecord(input.releaseGates) ? input.releaseGates : {};
45
149
  const releaseGateCriteria = rubric.releaseGates.map((gate) =>
46
- calculateReleaseGate(gate, input.releaseGates?.[gate.id], evidenceById),
150
+ calculateReleaseGate(gate, releaseGateInputs[gate.id], evidenceById),
47
151
  );
48
152
  const releaseGate = calculateAggregateReleaseGate(releaseGateCriteria);
49
153
  const findings = allocateFindingImpacts(
@@ -72,9 +176,7 @@ export async function calculateAudit(input) {
72
176
  releaseGateCriteria,
73
177
  ruleResults,
74
178
  findings,
75
- risks: Array.isArray(input.risks)
76
- ? input.risks.map((risk) => ({ ...risk, scoreImpact: 0 }))
77
- : [],
179
+ risks: Array.isArray(input.risks) ? input.risks.map(normalizeRisk) : [],
78
180
  passedRules: ruleResults
79
181
  .filter((rule) => rule.assessmentFraction === 1 && rule.status === 'pass')
80
182
  .map((rule) => rule.rule),
@@ -91,7 +193,7 @@ export async function calculateAudit(input) {
91
193
  }
92
194
 
93
195
  /*** Serialize one stable configuration or audit artifact using JSON-compatible YAML frontmatter. */
94
- export function serializeArtifact(input, audit) {
196
+ export function serializeArtifact(input: unknown, audit: unknown) {
95
197
  assertRecord(input, 'Artifact input');
96
198
  const documentKind = input.documentKind === 'audit' ? 'audit' : 'configuration';
97
199
  const frontmatter = {
@@ -147,13 +249,18 @@ export function serializeArtifact(input, audit) {
147
249
  }
148
250
 
149
251
  /*** Calculate all four canonical criteria and derived values for one weighted rule. */
150
- function calculateRule(rule, rawAssessments, evidenceById, statusFactors) {
252
+ function calculateRule(
253
+ rule: RuleDefinition,
254
+ rawAssessments: unknown,
255
+ evidenceById: Map<string, EvidenceItem>,
256
+ statusFactors: Record<string, number | null>,
257
+ ): RuleResult {
151
258
  const assessmentRecord = isRecord(rawAssessments) ? rawAssessments : {};
152
259
  const criteria = rule.criteria.map((criterionId) =>
153
260
  normalizeCriterion(criterionId, assessmentRecord[criterionId], evidenceById, statusFactors),
154
261
  );
155
262
  const applicable = criteria.filter((criterion) => criterion.applicable);
156
- const assessed = applicable.filter((criterion) => criterion.statusFactor !== null);
263
+ const assessed = applicable.filter(isAssessedCriterion);
157
264
  const applicableCount = applicable.length;
158
265
  const assessedCount = assessed.length;
159
266
  const assessmentFraction = applicableCount === 0 ? null : assessedCount / applicableCount;
@@ -190,7 +297,12 @@ function calculateRule(rule, rawAssessments, evidenceById, statusFactors) {
190
297
  }
191
298
 
192
299
  /*** Normalize one criterion and derive confidence only from its essential evidence. */
193
- function normalizeCriterion(criterionId, rawAssessment, evidenceById, statusFactors) {
300
+ function normalizeCriterion(
301
+ criterionId: string,
302
+ rawAssessment: unknown,
303
+ evidenceById: Map<string, EvidenceItem>,
304
+ statusFactors: Record<string, number | null>,
305
+ ): CriterionResult {
194
306
  const assessment = isRecord(rawAssessment) ? rawAssessment : {};
195
307
  const applicable = assessment.applicable !== false;
196
308
  const requestedStatus =
@@ -223,7 +335,7 @@ function normalizeCriterion(criterionId, rawAssessment, evidenceById, statusFact
223
335
  ? null
224
336
  : Math.min(
225
337
  ...essentialEvidenceIds.map(
226
- (evidenceId) => evidenceById.get(evidenceId).confidenceFactor,
338
+ (evidenceId) => evidenceById.get(evidenceId)?.confidenceFactor ?? 0,
227
339
  ),
228
340
  );
229
341
  return {
@@ -244,7 +356,7 @@ function normalizeCriterion(criterionId, rawAssessment, evidenceById, statusFact
244
356
  }
245
357
 
246
358
  /*** Calculate aggregate score, coverage, range, and confidence from rule results. */
247
- function calculateTotals(ruleResults) {
359
+ function calculateTotals(ruleResults: RuleResult[]): AuditTotals {
248
360
  const applicableRules = ruleResults.filter((rule) => rule.applicableCount > 0);
249
361
  const applicableWeight = applicableRules.reduce((sum, rule) => sum + rule.weight, 0);
250
362
  const assessedWeight = ruleResults.reduce(
@@ -260,7 +372,7 @@ function calculateTotals(ruleResults) {
260
372
  ? 0
261
373
  : (rule.weight / rule.applicableCount) *
262
374
  rule.criteria
263
- .filter((criterion) => criterion.statusFactor !== null)
375
+ .filter(isAssessedCriterion)
264
376
  .reduce((criterionSum, criterion) => criterionSum + criterion.statusFactor, 0)),
265
377
  0,
266
378
  );
@@ -287,14 +399,14 @@ function calculateTotals(ruleResults) {
287
399
  };
288
400
  }
289
401
  const confidenceNumerator = ruleResults.reduce((sum, rule) => {
290
- const applicableCount = rule.applicableCount;
402
+ const { applicableCount } = rule;
291
403
  if (applicableCount === 0) return sum;
292
404
  const criterionWeight = rule.weight / applicableCount;
293
405
  return (
294
406
  sum +
295
407
  criterionWeight *
296
408
  rule.criteria
297
- .filter((criterion) => criterion.statusFactor !== null)
409
+ .filter(isAssessedCriterion)
298
410
  .reduce((criterionSum, criterion) => criterionSum + criterion.confidenceFactor, 0)
299
411
  );
300
412
  }, 0);
@@ -315,10 +427,14 @@ function calculateTotals(ruleResults) {
315
427
  }
316
428
 
317
429
  /*** Validate evidence identities and canonical confidence factors. */
318
- function validateEvidence(evidence, confidenceFactors) {
319
- const evidenceById = new Map();
430
+ function validateEvidence(
431
+ evidence: unknown[],
432
+ confidenceFactors: Record<string, number>,
433
+ ): Map<string, EvidenceItem> {
434
+ const evidenceById = new Map<string, EvidenceItem>();
320
435
  for (const item of evidence) {
321
436
  assertRecord(item, 'Evidence item');
437
+ assertEvidenceItem(item);
322
438
  if (typeof item.id !== 'string' || item.id === '') {
323
439
  throw new Error('Every evidence item requires a non-empty id.');
324
440
  }
@@ -355,17 +471,17 @@ function validateEvidence(evidence, confidenceFactors) {
355
471
  }
356
472
 
357
473
  /*** Derive one canonical release item from all of its subcriteria. */
358
- function calculateReleaseGate(gate, rawGate, evidenceById) {
474
+ function calculateReleaseGate(
475
+ gate: ReleaseGateDefinition,
476
+ rawGate: unknown,
477
+ evidenceById: Map<string, EvidenceItem>,
478
+ ): ReleaseGateResult {
359
479
  const gateInput = isRecord(rawGate) ? rawGate : {};
360
480
  const rawCriteria = isRecord(gateInput.criteria) ? gateInput.criteria : {};
361
481
  const criteria = gate.criteria.map((criterionId) => {
362
482
  const raw = isRecord(rawCriteria[criterionId]) ? rawCriteria[criterionId] : {};
363
483
  const applicable = raw.applicable !== false;
364
- const status = applicable
365
- ? ['pass', 'fail'].includes(raw.status)
366
- ? raw.status
367
- : 'not-assessable'
368
- : 'not-applicable';
484
+ const status = applicable ? readReleaseCriterionStatus(raw.status) : 'not-applicable';
369
485
  const evidenceIds = readStringArray(raw.evidenceIds);
370
486
  if (['pass', 'fail'].includes(status) && evidenceIds.length === 0) {
371
487
  throw new Error(`Release criterion ${criterionId} requires evidence for status ${status}.`);
@@ -406,7 +522,7 @@ function calculateReleaseGate(gate, rawGate, evidenceById) {
406
522
  }
407
523
 
408
524
  /*** Derive the aggregate release gate without allowing the score to override evidence gaps. */
409
- function calculateAggregateReleaseGate(releaseGates) {
525
+ function calculateAggregateReleaseGate(releaseGates: ReleaseGateResult[]): string {
410
526
  const applicable = releaseGates.filter((gate) => gate.applicable);
411
527
  if (applicable.length === 0) return 'not-assessable';
412
528
  if (applicable.some((gate) => gate.status === 'fail')) return 'fail';
@@ -415,29 +531,44 @@ function calculateAggregateReleaseGate(releaseGates) {
415
531
  }
416
532
 
417
533
  /*** Deduplicate findings by root cause and allocate each criterion loss in stable units. */
418
- function allocateFindingImpacts(findings, ruleResults, applicableWeight, evidenceById) {
419
- const deduplicated = [];
420
- const seenRootCauses = new Set();
421
- for (const finding of [...findings].sort((left, right) => left.id.localeCompare(right.id))) {
534
+ function allocateFindingImpacts(
535
+ findings: unknown[],
536
+ ruleResults: RuleResult[],
537
+ applicableWeight: number,
538
+ evidenceById: Map<string, EvidenceItem>,
539
+ ) {
540
+ const normalizedFindings = findings.map((finding) => {
422
541
  assertRecord(finding, 'Finding');
423
542
  validateFinding(finding, evidenceById);
543
+ return finding;
544
+ });
545
+ const deduplicated: Finding[] = [];
546
+ const seenRootCauses = new Set<string>();
547
+ for (const finding of normalizedFindings.sort((left, right) => left.id.localeCompare(right.id))) {
424
548
  const rootCause = typeof finding.rootCause === 'string' ? finding.rootCause : finding.id;
425
549
  if (!seenRootCauses.has(rootCause)) {
426
550
  seenRootCauses.add(rootCause);
427
551
  deduplicated.push({ ...finding, rootCause });
428
552
  }
429
553
  }
430
- const byCriterion = new Map();
554
+ const byCriterion = new Map<string, Finding[]>();
431
555
  for (const finding of deduplicated) {
432
556
  const key = `${finding.rule}:${finding.criterionId}`;
433
557
  byCriterion.set(key, [...(byCriterion.get(key) ?? []), finding]);
434
558
  }
435
- const allocations = new Map();
559
+ const allocations = new Map<string, number>();
436
560
  for (const [key, criterionFindings] of byCriterion) {
437
561
  const [ruleId, criterionId] = key.split(':');
438
562
  const rule = ruleResults.find((result) => result.rule === ruleId);
439
- const criterion = rule?.criteria.find((result) => result.id === criterionId);
440
- if (!rule || !criterion || criterion.statusFactor === null || applicableWeight === 0) {
563
+ if (!rule) {
564
+ throw new Error(`Finding references an unscored criterion: ${key}`);
565
+ }
566
+ const criterion = rule.criteria.find((result) => result.id === criterionId);
567
+ if (
568
+ criterion?.statusFactor === undefined ||
569
+ criterion.statusFactor === null ||
570
+ applicableWeight === 0
571
+ ) {
441
572
  throw new Error(`Finding references an unscored criterion: ${key}`);
442
573
  }
443
574
  if (criterion.statusFactor === 1) {
@@ -460,7 +591,10 @@ function allocateFindingImpacts(findings, ruleResults, applicableWeight, evidenc
460
591
  }
461
592
 
462
593
  /*** Validate the complete finding contract and its evidence references before score allocation. */
463
- function validateFinding(finding, evidenceById) {
594
+ function validateFinding(
595
+ finding: Record<string, unknown>,
596
+ evidenceById: Map<string, EvidenceItem>,
597
+ ): asserts finding is Finding {
464
598
  for (const field of [
465
599
  'id',
466
600
  'rule',
@@ -476,26 +610,32 @@ function validateFinding(finding, evidenceById) {
476
610
  'verification',
477
611
  'confidence',
478
612
  ]) {
479
- if (typeof finding[field] !== 'string' || finding[field] === '') {
613
+ const fieldValue = finding[field];
614
+ if (typeof fieldValue !== 'string' || fieldValue === '') {
480
615
  throw new Error(`Finding requires a non-empty ${field}.`);
481
616
  }
482
617
  }
618
+ const findingId = typeof finding.id === 'string' ? finding.id : 'unknown finding';
483
619
  if (!Array.isArray(finding.relatedRules)) {
484
- throw new Error(`Finding ${finding.id} requires a relatedRules list.`);
620
+ throw new Error(`Finding ${findingId} requires a relatedRules list.`);
485
621
  }
486
622
  const evidenceIds = readStringArray(finding.evidenceIds);
487
623
  if (evidenceIds.length === 0) {
488
- throw new Error(`Finding ${finding.id} requires evidenceIds.`);
624
+ throw new Error(`Finding ${findingId} requires evidenceIds.`);
489
625
  }
490
626
  for (const evidenceId of evidenceIds) {
491
627
  if (!evidenceById.has(evidenceId)) {
492
- throw new Error(`Finding ${finding.id} references unknown evidence: ${evidenceId}`);
628
+ throw new Error(`Finding ${findingId} references unknown evidence: ${evidenceId}`);
493
629
  }
494
630
  }
495
631
  }
496
632
 
497
633
  /*** Label rule status from assessed factors while preserving explicit critical evidence. */
498
- function labelRuleStatus(criteria, meanFactor, applicableCount) {
634
+ function labelRuleStatus(
635
+ criteria: CriterionResult[],
636
+ meanFactor: number | null,
637
+ applicableCount: number,
638
+ ): string {
499
639
  if (applicableCount === 0) return 'not-applicable';
500
640
  if (meanFactor === null) return 'not-assessable';
501
641
  if (criteria.some((criterion) => criterion.status === 'critical')) return 'critical';
@@ -506,7 +646,7 @@ function labelRuleStatus(criteria, meanFactor, applicableCount) {
506
646
  }
507
647
 
508
648
  /*** Label evidence coverage without conflating it with confidence. */
509
- function labelCoverage(value) {
649
+ function labelCoverage(value: number | null): string | null {
510
650
  if (value === null) return null;
511
651
  if (value >= 85) return 'high';
512
652
  if (value >= 60) return 'medium';
@@ -514,7 +654,7 @@ function labelCoverage(value) {
514
654
  }
515
655
 
516
656
  /*** Label aggregate evidence confidence from the canonical numeric thresholds. */
517
- function labelConfidence(value) {
657
+ function labelConfidence(value: number | null): string | null {
518
658
  if (value === null) return null;
519
659
  if (value >= 0.8) return 'high';
520
660
  if (value >= 0.5) return 'medium';
@@ -522,28 +662,118 @@ function labelConfidence(value) {
522
662
  }
523
663
 
524
664
  /*** Round a nonnegative displayed value half up. */
525
- function roundHalfUp(value) {
665
+ function roundHalfUp(value: number): number {
526
666
  return Math.floor(value + 0.5);
527
667
  }
528
668
 
529
669
  /*** Round an intermediate serialization value without changing calculation inputs. */
530
- function roundDecimal(value, digits) {
670
+ function roundDecimal(value: number, digits: number): number {
531
671
  const factor = 10 ** digits;
532
672
  return Math.round((value + Number.EPSILON) * factor) / factor;
533
673
  }
534
674
 
675
+ /*** Return whether a criterion carries the numeric factors required for scoring. */
676
+ function isAssessedCriterion(
677
+ criterion: CriterionResult,
678
+ ): criterion is CriterionResult & { confidenceFactor: number; statusFactor: number } {
679
+ return criterion.statusFactor !== null && criterion.confidenceFactor !== null;
680
+ }
681
+
682
+ /*** Normalize one risk while preserving its supplied descriptive fields. */
683
+ function normalizeRisk(value: unknown): Record<string, unknown> {
684
+ assertRecord(value, 'Risk');
685
+ return { ...value, scoreImpact: 0 };
686
+ }
687
+
688
+ /*** Read the only evidence-backed release criterion statuses. */
689
+ function readReleaseCriterionStatus(value: unknown): string {
690
+ return value === 'pass' || value === 'fail' ? value : 'not-assessable';
691
+ }
692
+
693
+ /*** Validate the canonical rubric structure loaded from the packaged JSON asset. */
694
+ function assertAuditRubric(value: unknown): asserts value is AuditRubric {
695
+ assertRecord(value, 'Audit rubric');
696
+ if (!isNumberRecord(value.confidenceFactors) || !isNullableNumberRecord(value.statusFactors)) {
697
+ throw new Error('Audit rubric factors must be numeric records.');
698
+ }
699
+ if (!Array.isArray(value.rules) || !Array.isArray(value.releaseGates)) {
700
+ throw new Error('Audit rubric requires rules and releaseGates arrays.');
701
+ }
702
+ for (const rule of value.rules) {
703
+ assertRecord(rule, 'Audit rule');
704
+ if (
705
+ typeof rule.id !== 'string' ||
706
+ typeof rule.name !== 'string' ||
707
+ typeof rule.weight !== 'number' ||
708
+ !isStringArray(rule.criteria)
709
+ ) {
710
+ throw new Error('Every audit rule requires id, name, weight, and criteria.');
711
+ }
712
+ }
713
+ for (const gate of value.releaseGates) {
714
+ assertRecord(gate, 'Release gate');
715
+ if (
716
+ typeof gate.id !== 'string' ||
717
+ typeof gate.name !== 'string' ||
718
+ !isStringArray(gate.criteria)
719
+ ) {
720
+ throw new Error('Every release gate requires id, name, and criteria.');
721
+ }
722
+ }
723
+ }
724
+
725
+ /*** Validate one evidence record before semantic checks use its required fields. */
726
+ function assertEvidenceItem(value: Record<string, unknown>): asserts value is EvidenceItem {
727
+ const stringFields = [
728
+ 'confidence',
729
+ 'evidenceLevel',
730
+ 'id',
731
+ 'kind',
732
+ 'location',
733
+ 'observation',
734
+ 'reproduction',
735
+ ];
736
+ if (
737
+ stringFields.some((field) => typeof value[field] !== 'string') ||
738
+ typeof value.confidenceFactor !== 'number' ||
739
+ !Array.isArray(value.limitations)
740
+ ) {
741
+ throw new Error('Evidence item has an invalid canonical shape.');
742
+ }
743
+ }
744
+
745
+ /*** Return whether every record value is a finite number. */
746
+ function isNumberRecord(value: unknown): value is Record<string, number> {
747
+ return isRecord(value) && Object.values(value).every((item) => typeof item === 'number');
748
+ }
749
+
750
+ /*** Return whether every record value is numeric or explicitly unassessed. */
751
+ function isNullableNumberRecord(value: unknown): value is Record<string, number | null> {
752
+ return (
753
+ isRecord(value) &&
754
+ Object.values(value).every((item) => item === null || typeof item === 'number')
755
+ );
756
+ }
757
+
758
+ /*** Return whether an unknown value contains only strings. */
759
+ function isStringArray(value: unknown): value is string[] {
760
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
761
+ }
762
+
535
763
  /*** Read a stable string list from optional audit input. */
536
- function readStringArray(value) {
537
- return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
764
+ function readStringArray(value: unknown): string[] {
765
+ return Array.isArray(value)
766
+ ? value.filter((item): item is string => typeof item === 'string')
767
+ : [];
538
768
  }
539
769
 
540
770
  /*** Narrow an unknown value to a non-array record. */
541
- function isRecord(value) {
771
+ function isRecord(value: unknown): value is Record<string, unknown> {
542
772
  return typeof value === 'object' && value !== null && !Array.isArray(value);
543
773
  }
544
774
 
545
775
  /*** Require an object-shaped input value. */
546
- function assertRecord(value, label) {
776
+ function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown> {
547
777
  if (!isRecord(value)) throw new Error(`${label} must be an object.`);
548
778
  }
549
779
 
@@ -555,14 +785,15 @@ async function main() {
555
785
  return;
556
786
  }
557
787
  if (command === 'audit' && inputPath) {
558
- const input = JSON.parse(await readFile(inputPath, 'utf8'));
788
+ const input: unknown = JSON.parse(await readFile(inputPath, 'utf8'));
789
+ assertRecord(input, 'Audit artifact input');
559
790
  const audit = await calculateAudit(input.auditInput ?? input);
560
791
  const artifact = serializeArtifact({ ...input, documentKind: 'audit' }, audit);
561
792
  if (outputPath) await writeFile(outputPath, artifact);
562
793
  else console.log(artifact);
563
794
  return;
564
795
  }
565
- throw new Error('Usage: audit.mjs catalog | audit.mjs audit <input.json> [output.md]');
796
+ throw new Error('Usage: audit.ts catalog | audit.ts audit <input.json> [output.md]');
566
797
  }
567
798
 
568
799
  if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
@@ -3,16 +3,25 @@
3
3
  import { access, readdir, writeFile } from 'node:fs/promises';
4
4
  import { join, resolve } from 'node:path';
5
5
 
6
- import { loadContractsApi } from './owner-api.mjs';
6
+ import { loadContractsApi } from './owner-api.ts';
7
7
 
8
8
  const CATEGORY_ROOT = 'src/templates/categories';
9
9
 
10
+ interface TemplateDefinitionSource {
11
+ category: string;
12
+ categoryDirectory: string;
13
+ slug: string;
14
+ }
15
+
10
16
  /*** Regenerate the portable template catalog from canonical template directories. */
11
- export async function generateTemplateCatalog(targetDirectory = process.cwd(), appCategories) {
17
+ export async function generateTemplateCatalog(
18
+ targetDirectory = process.cwd(),
19
+ appCategories?: readonly string[],
20
+ ): Promise<{ outputPath: string; templateCount: number }> {
12
21
  const root = resolve(targetDirectory);
13
22
  const canonicalAppCategories = appCategories ?? (await loadContractsApi(root)).APP_CATEGORIES;
14
23
  const categoriesRoot = join(root, CATEGORY_ROOT);
15
- const definitions = [];
24
+ const definitions: TemplateDefinitionSource[] = [];
16
25
 
17
26
  for (const categoryEntry of await readDirectories(categoriesRoot)) {
18
27
  const category = categoryEntry.name.replaceAll('-', '_');
@@ -65,14 +74,14 @@ export const TEMPLATE_DEFINITIONS: readonly TemplateDefinition[] = ${definitions
65
74
  }
66
75
 
67
76
  /*** Read child directories in stable lexical order. */
68
- async function readDirectories(directory) {
77
+ async function readDirectories(directory: string) {
69
78
  return (await readdir(directory, { withFileTypes: true }))
70
79
  .filter((entry) => entry.isDirectory())
71
80
  .sort((left, right) => left.name.localeCompare(right.name));
72
81
  }
73
82
 
74
83
  /*** Return whether a filesystem path exists. */
75
- async function pathExists(filePath) {
84
+ async function pathExists(filePath: string): Promise<boolean> {
76
85
  try {
77
86
  await access(filePath);
78
87
  return true;