arkaik 0.1.2 → 0.3.0

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/index.js CHANGED
@@ -14942,6 +14942,7 @@ function crossCheckJournal(bundle) {
14942
14942
  });
14943
14943
  const everNodes = new Set(snapshotNodeStatus.keys());
14944
14944
  const everEdges = new Set(snapshotEdgeIds);
14945
+ const baselined = /* @__PURE__ */ new Set();
14945
14946
  for (const { ev } of valid) {
14946
14947
  if (ev.type === "node.created") {
14947
14948
  const nid = str2(ev.node_id);
@@ -14949,6 +14950,14 @@ function crossCheckJournal(bundle) {
14949
14950
  } else if (ev.type === "edge.added") {
14950
14951
  const eid = str2(ev.edge_id);
14951
14952
  if (eid) everEdges.add(eid);
14953
+ } else if (ev.type === "journal.baseline" && Array.isArray(ev.node_ids)) {
14954
+ for (const raw of ev.node_ids) {
14955
+ const nid = str2(raw);
14956
+ if (nid) {
14957
+ baselined.add(nid);
14958
+ everNodes.add(nid);
14959
+ }
14960
+ }
14952
14961
  }
14953
14962
  }
14954
14963
  const ordered = orderEvents(valid.map((v) => v.ev));
@@ -15014,7 +15023,7 @@ function crossCheckJournal(bundle) {
15014
15023
  }
15015
15024
  }
15016
15025
  for (const [nodeId, status] of snapshotNodeStatus) {
15017
- if (!created.has(nodeId)) {
15026
+ if (!created.has(nodeId) && !baselined.has(nodeId)) {
15018
15027
  findings.push({
15019
15028
  path: "journal",
15020
15029
  rule: "journal-missing-node-created",
@@ -15046,6 +15055,26 @@ function crossCheckJournal(bundle) {
15046
15055
  }
15047
15056
  return findings;
15048
15057
  }
15058
+ function missingProvenanceNodeIds(snapshotNodeIds, events) {
15059
+ const covered = /* @__PURE__ */ new Set();
15060
+ for (const ev of events) {
15061
+ if (ev.type === "node.created") {
15062
+ if (typeof ev.node_id === "string") covered.add(ev.node_id);
15063
+ } else if (ev.type === "journal.baseline" && Array.isArray(ev.node_ids)) {
15064
+ for (const id of ev.node_ids) {
15065
+ if (typeof id === "string") covered.add(id);
15066
+ }
15067
+ }
15068
+ }
15069
+ const missing = [];
15070
+ const seen = /* @__PURE__ */ new Set();
15071
+ for (const id of snapshotNodeIds) {
15072
+ if (covered.has(id) || seen.has(id)) continue;
15073
+ seen.add(id);
15074
+ missing.push(id);
15075
+ }
15076
+ return missing;
15077
+ }
15049
15078
 
15050
15079
  // ../schema/src/journal-events.ts
15051
15080
  var envelope = {
@@ -15112,7 +15141,12 @@ var DeliverableShippedEventSchema = external_exports.object({
15112
15141
  summary: external_exports.string().optional(),
15113
15142
  url: external_exports.string().optional(),
15114
15143
  node_ids: external_exports.array(external_exports.string()).optional(),
15115
- platform: PlatformSchema.optional()
15144
+ platform: PlatformSchema.optional(),
15145
+ lab_note: external_exports.object({
15146
+ en: external_exports.object({ title: external_exports.string(), summary: external_exports.string() }).catchall(external_exports.unknown()),
15147
+ fr: external_exports.object({ title: external_exports.string().optional(), summary: external_exports.string().optional() }).catchall(external_exports.unknown()).optional(),
15148
+ suggested: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
15149
+ }).catchall(external_exports.unknown()).optional().meta({ description: "The Lab Note a merged PR carried (slice 3); en.title/en.summary required when present." })
15116
15150
  }).catchall(external_exports.unknown());
15117
15151
  var IdeaProposedEventSchema = external_exports.object({
15118
15152
  ...envelope,
@@ -15147,6 +15181,49 @@ var RefStatusChangedEventSchema = external_exports.object({
15147
15181
  to: external_exports.string(),
15148
15182
  synced_at: external_exports.string()
15149
15183
  }).catchall(external_exports.unknown());
15184
+ var JournalBaselineEventSchema = external_exports.object({ ...envelope, type: external_exports.literal("journal.baseline"), node_ids: external_exports.array(external_exports.string()) }).catchall(external_exports.unknown());
15185
+ var QualityAuditCompletedEventSchema = external_exports.object({
15186
+ ...envelope,
15187
+ type: external_exports.literal("quality.audit.completed"),
15188
+ audit_id: external_exports.string(),
15189
+ framework_version: external_exports.string(),
15190
+ commit: external_exports.string().optional(),
15191
+ scores: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.number())).optional(),
15192
+ counts: external_exports.object({
15193
+ critical: external_exports.number().optional(),
15194
+ high: external_exports.number().optional(),
15195
+ medium: external_exports.number().optional(),
15196
+ low: external_exports.number().optional(),
15197
+ info: external_exports.number().optional()
15198
+ }).catchall(external_exports.unknown()).optional()
15199
+ }).catchall(external_exports.unknown());
15200
+ var QualityFindingOpenedEventSchema = external_exports.object({
15201
+ ...envelope,
15202
+ type: external_exports.literal("quality.finding.opened"),
15203
+ finding_id: external_exports.string(),
15204
+ criterion_id: external_exports.string(),
15205
+ surface: external_exports.string(),
15206
+ severity: external_exports.string(),
15207
+ priority: external_exports.string(),
15208
+ title: external_exports.string(),
15209
+ node_ids: external_exports.array(external_exports.string()).optional(),
15210
+ issue_url: external_exports.string().optional()
15211
+ }).catchall(external_exports.unknown());
15212
+ var QualityFindingResolvedEventSchema = external_exports.object({
15213
+ ...envelope,
15214
+ type: external_exports.literal("quality.finding.resolved"),
15215
+ finding_id: external_exports.string(),
15216
+ resolved_by: external_exports.string().optional(),
15217
+ node_ids: external_exports.array(external_exports.string()).optional()
15218
+ }).catchall(external_exports.unknown());
15219
+ var QualitySignalTrippedEventSchema = external_exports.object({
15220
+ ...envelope,
15221
+ type: external_exports.literal("quality.signal.tripped"),
15222
+ criterion_id: external_exports.string(),
15223
+ surface: external_exports.string(),
15224
+ signal: external_exports.string(),
15225
+ detail: external_exports.string().optional()
15226
+ }).catchall(external_exports.unknown());
15150
15227
  var JOURNAL_EVENT_SCHEMAS = {
15151
15228
  "node.created": NodeCreatedEventSchema,
15152
15229
  "node.updated": NodeUpdatedEventSchema,
@@ -15161,7 +15238,12 @@ var JOURNAL_EVENT_SCHEMAS = {
15161
15238
  "request.filed": RequestFiledEventSchema,
15162
15239
  "ref.added": RefAddedEventSchema,
15163
15240
  "ref.removed": RefRemovedEventSchema,
15164
- "ref.status_changed": RefStatusChangedEventSchema
15241
+ "ref.status_changed": RefStatusChangedEventSchema,
15242
+ "journal.baseline": JournalBaselineEventSchema,
15243
+ "quality.audit.completed": QualityAuditCompletedEventSchema,
15244
+ "quality.finding.opened": QualityFindingOpenedEventSchema,
15245
+ "quality.finding.resolved": QualityFindingResolvedEventSchema,
15246
+ "quality.signal.tripped": QualitySignalTrippedEventSchema
15165
15247
  };
15166
15248
  var KnownJournalEventSchema = external_exports.union([
15167
15249
  NodeCreatedEventSchema,
@@ -15177,9 +15259,114 @@ var KnownJournalEventSchema = external_exports.union([
15177
15259
  RequestFiledEventSchema,
15178
15260
  RefAddedEventSchema,
15179
15261
  RefRemovedEventSchema,
15180
- RefStatusChangedEventSchema
15262
+ RefStatusChangedEventSchema,
15263
+ JournalBaselineEventSchema,
15264
+ QualityAuditCompletedEventSchema,
15265
+ QualityFindingOpenedEventSchema,
15266
+ QualityFindingResolvedEventSchema,
15267
+ QualitySignalTrippedEventSchema
15181
15268
  ]);
15182
15269
 
15270
+ // ../schema/src/quality-schemas.ts
15271
+ var GradeSchema = external_exports.enum(["A", "B", "C", "D", "E"]);
15272
+ var KritikScalesSchema = external_exports.object({
15273
+ grades: external_exports.partialRecord(GradeSchema, external_exports.number()).optional(),
15274
+ severity_buckets: external_exports.partialRecord(external_exports.enum(["critical", "high", "medium", "low", "info"]), external_exports.tuple([external_exports.number(), external_exports.number()])).optional(),
15275
+ caps: external_exports.object({ critical_open: GradeSchema.optional(), high_open: GradeSchema.optional() }).catchall(external_exports.unknown()).optional()
15276
+ }).catchall(external_exports.unknown()).meta({
15277
+ id: "KritikScales",
15278
+ description: "Overridable maturity/severity/grade scales carried by a criteria pack. Absent entries fall back to the framework defaults (packages/kritik-library/SPEC.md \xA7 4)."
15279
+ });
15280
+ var KritikReferenceSchema = external_exports.object({ title: external_exports.string(), anchor: external_exports.string().optional(), url: external_exports.string().optional() }).catchall(external_exports.unknown()).meta({ id: "KritikReference", description: "A stable external anchor behind a criterion (OWASP control, GDPR article, WCAG SC\u2026)." });
15281
+ var KritikIssueTemplateSchema = external_exports.object({
15282
+ title_template: external_exports.string().optional(),
15283
+ labels: external_exports.array(external_exports.string()).optional(),
15284
+ body_skeleton: external_exports.string().optional()
15285
+ }).catchall(external_exports.unknown()).meta({ id: "KritikIssueTemplate", description: "Per-criterion issue skeleton \u2014 what `arkaik kritik issue` prefills." });
15286
+ var KritikDomainSchema = external_exports.object({ code: external_exports.string(), name: external_exports.string(), description: external_exports.string().optional() }).catchall(external_exports.unknown()).meta({ id: "KritikDomain", description: "A top-level criteria category owning a roll-up row in the matrix." });
15287
+ var KritikCriterionSchema = external_exports.object({
15288
+ id: external_exports.string().meta({ description: "Stable identifier, `<DOMAIN>-NN`. Never redefined \u2014 retired via superseded_by." }),
15289
+ domain: external_exports.string(),
15290
+ subcategory: external_exports.string().optional(),
15291
+ name: external_exports.string().optional(),
15292
+ question: external_exports.string().optional(),
15293
+ definition: external_exports.string().optional(),
15294
+ rationale: external_exports.string().optional(),
15295
+ applies_to: external_exports.array(external_exports.string()).optional().meta({ description: "Surface ids where the criterion is meaningful; absence means N/A." }),
15296
+ level_anchors: external_exports.record(external_exports.string(), external_exports.string()).optional().meta({ description: "Observable descriptions per maturity level, keyed l0\u2026l4." }),
15297
+ default_impact: external_exports.number().optional(),
15298
+ weight: external_exports.number().optional().meta({ description: "Weight in the domain roll-up (1\u20133; 3 = load-bearing). Absent means 1." }),
15299
+ references: external_exports.array(KritikReferenceSchema).optional(),
15300
+ checklist: external_exports.array(external_exports.string()).optional(),
15301
+ signals: external_exports.array(external_exports.string()).optional().meta({ description: "Mechanically checkable monitoring hooks run between audits." }),
15302
+ remediation: external_exports.string().optional(),
15303
+ issue: KritikIssueTemplateSchema.optional(),
15304
+ superseded_by: external_exports.string().optional()
15305
+ }).catchall(external_exports.unknown()).meta({ id: "KritikCriterion", description: "The atomic auditable unit (packages/kritik-library/SPEC.md \xA7 5)." });
15306
+ var KritikLibrarySchema = external_exports.object({
15307
+ version: external_exports.string().meta({ description: "Semver of the pack; matrices are comparable only within a major." }),
15308
+ domains: external_exports.array(KritikDomainSchema),
15309
+ criteria: external_exports.array(KritikCriterionSchema),
15310
+ scales: KritikScalesSchema.optional()
15311
+ }).catchall(external_exports.unknown()).meta({
15312
+ id: "KritikLibrary",
15313
+ description: "A versioned criteria pack. Embedded on export; repos may instead pin it as a sidecar file."
15314
+ });
15315
+ var SurfaceDefSchema = external_exports.object({
15316
+ id: external_exports.string(),
15317
+ title: external_exports.string(),
15318
+ platform: PlatformSchema.optional().meta({
15319
+ description: "Optional bridge to PLATFORM_IDS where a surface is also a view-shipping platform. Surfaces are not platforms (RFC \xA7 2)."
15320
+ }),
15321
+ path: external_exports.string().optional()
15322
+ }).catchall(external_exports.unknown()).meta({ id: "SurfaceDef", description: "One independently assessable body of code \u2014 an audit target, chosen at install." });
15323
+ var QualityProfileSchema = external_exports.object({
15324
+ surfaces: external_exports.array(SurfaceDefSchema),
15325
+ domain_weights: external_exports.record(external_exports.string(), external_exports.number()).optional().meta({
15326
+ description: "Per-domain weight in the surface roll-up, keyed by domain code. A missing weight is 1."
15327
+ })
15328
+ }).catchall(external_exports.unknown()).meta({ id: "QualityProfile", description: "The project's parameterization of the pack (RFC \xA7 6)." });
15329
+ var QualityAssessmentSchema = external_exports.object({
15330
+ criterion_id: external_exports.string(),
15331
+ surface: external_exports.string(),
15332
+ level: external_exports.union([external_exports.literal(0), external_exports.literal(1), external_exports.literal(2), external_exports.literal(3), external_exports.literal(4)]).meta({
15333
+ description: "Maturity 0\u20134. N/A is the absence of the row, never a level."
15334
+ }),
15335
+ evidence: external_exports.string().meta({ description: "file:line / config citations. A score without evidence is an opinion, not an assessment." }),
15336
+ audit_id: external_exports.string(),
15337
+ commit: external_exports.string().optional(),
15338
+ ts: external_exports.string()
15339
+ }).catchall(external_exports.unknown()).meta({ id: "QualityAssessment", description: "One (criterion \xD7 surface) score \u2014 latest wins; history lives in the journal." });
15340
+ var QualityFindingSchema = external_exports.object({
15341
+ id: external_exports.string(),
15342
+ criterion_id: external_exports.string(),
15343
+ surface: external_exports.string(),
15344
+ title: external_exports.string(),
15345
+ detail: external_exports.string(),
15346
+ evidence: external_exports.string(),
15347
+ impact: external_exports.number().meta({ description: "Worst plausible consequence, 1\u20135." }),
15348
+ likelihood: external_exports.number().meta({ description: "Probability it materializes, 1\u20135." }),
15349
+ cost: external_exports.enum(["S", "M", "L", "XL"]),
15350
+ status: external_exports.enum(["open", "resolved", "refuted", "accepted-risk"]),
15351
+ remediation: external_exports.string().optional(),
15352
+ node_ids: external_exports.array(external_exports.string()).optional().meta({ description: "Graph nodes this finding is about \u2014 the tie deliverable.shipped already uses." }),
15353
+ issue_url: external_exports.string().optional(),
15354
+ verification: external_exports.object({ verdict: external_exports.enum(["CONFIRMED", "REFUTED", "DOWNGRADED"]), note: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Result of the adversarial refutation pass. Refuted findings are disclosed, not deleted." })
15355
+ }).catchall(external_exports.unknown()).meta({
15356
+ id: "QualityFinding",
15357
+ description: "One concrete defect. Severity and priority are deliberately absent \u2014 they are derived from impact \xD7 likelihood and cost (deriveQualityMatrix), so a finding can never carry a severity its own numbers disagree with."
15358
+ });
15359
+ var QualitySectionSchema = external_exports.object({
15360
+ framework_version: external_exports.string(),
15361
+ library: KritikLibrarySchema.optional(),
15362
+ profile: QualityProfileSchema,
15363
+ assessments: external_exports.array(QualityAssessmentSchema),
15364
+ findings: external_exports.array(QualityFindingSchema)
15365
+ }).catchall(external_exports.unknown()).meta({
15366
+ id: "QualitySection",
15367
+ description: "The project's Kritik state (docs/rfcs/kritik.md \xA7 4.1) \u2014 profile, assessments, findings. Criteria are library content, not graph nodes; the matrix is a projection, never stored."
15368
+ });
15369
+
15183
15370
  // ../schema/src/bundle.ts
15184
15371
  var PlatformStatusMapSchema = external_exports.partialRecord(
15185
15372
  PlatformSchema,
@@ -15321,7 +15508,8 @@ var ProjectMetadataSchema = external_exports.object({
15321
15508
  }),
15322
15509
  products: external_exports.array(ProductDefinitionSchema).optional().meta({
15323
15510
  description: "Product definitions (docs/spec/bundle-format.md \xA7 Products) \u2014 additive; unknown fields preserved."
15324
- })
15511
+ }),
15512
+ pollen: external_exports.object({ plant: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/).optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Federation settings \u2014 docs/spec/services.md \xA7 Pollen feed. `plant` enables the feed." })
15325
15513
  }).catchall(external_exports.unknown()).meta({ id: "ProjectMetadata", description: "Optional project-level UI settings." });
15326
15514
  var ProjectSchema = external_exports.object({
15327
15515
  id: external_exports.string().meta({ description: "Unique project identifier." }),
@@ -15349,6 +15537,9 @@ var ProjectBundleSchema = external_exports.object({
15349
15537
  edges: external_exports.array(EdgeSchema).meta({ description: "All edges (relationships) between nodes." }),
15350
15538
  journal: external_exports.array(JournalEventSchema).optional().meta({
15351
15539
  description: "Optional embedded journal events \u2014 the Level 2 interchange projection (docs/spec/journal.md). Canonical storage in repos is the JSONL sidecar; a bundle without a journal is Level 0/1, not an error."
15540
+ }),
15541
+ quality: QualitySectionSchema.optional().meta({
15542
+ description: "Optional Kritik quality state (docs/rfcs/kritik.md \xA7 4.1) \u2014 the pinned pack version, the project's surface profile, its assessments and findings. Additive: a bundle without it is not an error, and the comparative matrix is derived from it (deriveQualityMatrix), never stored."
15352
15543
  })
15353
15544
  }).catchall(external_exports.unknown()).meta({
15354
15545
  id: "ProjectBundle",
@@ -15356,15 +15547,6 @@ var ProjectBundleSchema = external_exports.object({
15356
15547
  description: "The import/export format for an Arkaik product graph. Contains a project, its nodes (flows, views, data models, API endpoints), and edges (relationships between nodes)."
15357
15548
  });
15358
15549
 
15359
- // ../schema/src/maps.ts
15360
- var MAP_FLOW_PLATFORMS_MODES = ["rings", "bars"];
15361
- var MAP_VIEW_PLATFORMS_MODES = ["chips", "rows"];
15362
- var MAP_MINIMAP_COLOR_MODES = ["status", "species"];
15363
- var BUILT_IN_MAP_IDS = ["journey", "system"];
15364
- function isBuiltInMapId(id) {
15365
- return BUILT_IN_MAP_IDS.includes(id);
15366
- }
15367
-
15368
15550
  // ../schema/src/products.ts
15369
15551
  var PRODUCT_MEMBERSHIP_SPECIES = ["flow", "view", "acceptance"];
15370
15552
  function resolveProducts(project) {
@@ -15382,6 +15564,214 @@ function resolveProducts(project) {
15382
15564
  return products;
15383
15565
  }
15384
15566
 
15567
+ // ../schema/src/maps.ts
15568
+ var MAP_FLOW_PLATFORMS_MODES = ["rings", "bars"];
15569
+ var MAP_VIEW_PLATFORMS_MODES = ["chips", "rows"];
15570
+ var MAP_MINIMAP_COLOR_MODES = ["status", "species"];
15571
+ var BUILT_IN_MAP_IDS = ["journey", "system"];
15572
+ function isBuiltInMapId(id) {
15573
+ return BUILT_IN_MAP_IDS.includes(id);
15574
+ }
15575
+
15576
+ // ../schema/src/quality.ts
15577
+ var asArray = (value) => Array.isArray(value) ? value : [];
15578
+ var FINDING_SEVERITIES = ["critical", "high", "medium", "low", "info"];
15579
+ var REMEDIATION_COSTS = ["S", "M", "L", "XL"];
15580
+ var QUALITY_GRADES = ["A", "B", "C", "D", "E"];
15581
+ var DEFAULT_GRADE_BANDS = { A: 85, B: 70, C: 55, D: 40, E: 0 };
15582
+ var DEFAULT_SEVERITY_BUCKETS = {
15583
+ critical: [20, 25],
15584
+ high: [12, 19],
15585
+ medium: [6, 11],
15586
+ low: [2, 5],
15587
+ info: [1, 1]
15588
+ };
15589
+ var DEFAULT_CAPS = {
15590
+ critical_open: "D",
15591
+ high_open: "B"
15592
+ };
15593
+ var CROSS_SURFACE_ID = "cross-surface";
15594
+ function mergeKritikLibrary(pack, overlay) {
15595
+ if (!overlay || typeof overlay !== "object") return pack;
15596
+ const domains = [...asArray(pack.domains)];
15597
+ for (const domain2 of asArray(overlay.domains)) {
15598
+ if (typeof domain2?.code !== "string" || domain2.code === "") continue;
15599
+ const at2 = domains.findIndex((existing) => existing?.code === domain2.code);
15600
+ if (at2 >= 0) domains[at2] = domain2;
15601
+ else domains.push(domain2);
15602
+ }
15603
+ const criteria = [...asArray(pack.criteria)];
15604
+ for (const criterion of asArray(overlay.criteria)) {
15605
+ if (typeof criterion?.id !== "string" || criterion.id === "") continue;
15606
+ const at2 = criteria.findIndex((existing) => existing?.id === criterion.id);
15607
+ if (at2 >= 0) criteria[at2] = criterion;
15608
+ else criteria.push(criterion);
15609
+ }
15610
+ const scales = pack.scales || overlay.scales ? { ...pack.scales ?? {}, ...overlay.scales ?? {} } : void 0;
15611
+ return { ...pack, domains, criteria, ...scales ? { scales } : {} };
15612
+ }
15613
+ function bucketsOf(library) {
15614
+ const stored = library?.scales?.severity_buckets;
15615
+ const out = { ...DEFAULT_SEVERITY_BUCKETS };
15616
+ if (!stored || typeof stored !== "object") return out;
15617
+ for (const severity of FINDING_SEVERITIES) {
15618
+ const band = stored[severity];
15619
+ if (Array.isArray(band) && typeof band[0] === "number" && typeof band[1] === "number") {
15620
+ out[severity] = [band[0], band[1]];
15621
+ }
15622
+ }
15623
+ return out;
15624
+ }
15625
+ function bandsOf(library) {
15626
+ const stored = library?.scales?.grades;
15627
+ const out = { ...DEFAULT_GRADE_BANDS };
15628
+ if (!stored || typeof stored !== "object") return out;
15629
+ for (const grade of QUALITY_GRADES) {
15630
+ const min = stored[grade];
15631
+ if (typeof min === "number" && Number.isFinite(min)) out[grade] = min;
15632
+ }
15633
+ return out;
15634
+ }
15635
+ function capsOf(library) {
15636
+ const stored = library?.scales?.caps;
15637
+ const pick2 = (value, fallback) => typeof value === "string" && QUALITY_GRADES.includes(value) ? value : fallback;
15638
+ return {
15639
+ critical_open: pick2(stored?.critical_open, DEFAULT_CAPS.critical_open),
15640
+ high_open: pick2(stored?.high_open, DEFAULT_CAPS.high_open)
15641
+ };
15642
+ }
15643
+ function severityOf(finding, library) {
15644
+ const impact = typeof finding.impact === "number" ? finding.impact : 0;
15645
+ const likelihood = typeof finding.likelihood === "number" ? finding.likelihood : 0;
15646
+ const score = impact * likelihood;
15647
+ const buckets = bucketsOf(library);
15648
+ const worstFirst = [...FINDING_SEVERITIES].sort((a, b) => buckets[b][0] - buckets[a][0]);
15649
+ for (const severity of worstFirst) {
15650
+ if (score >= buckets[severity][0]) return severity;
15651
+ }
15652
+ return "info";
15653
+ }
15654
+ function priorityOf(finding, library) {
15655
+ const severity = severityOf(finding, library);
15656
+ const cheap = finding.cost === "S";
15657
+ if (severity === "critical") return "P0";
15658
+ if (severity === "high") return cheap ? "P0" : "P1";
15659
+ if (severity === "medium") return cheap ? "P1" : "P2";
15660
+ if (severity === "low") return cheap ? "P2" : "P3";
15661
+ return "P3";
15662
+ }
15663
+ function gradeOf(score, library) {
15664
+ const bands = bandsOf(library);
15665
+ const bestFirst = [...QUALITY_GRADES].sort((a, b) => bands[b] - bands[a]);
15666
+ for (const grade of bestFirst) {
15667
+ if (score >= bands[grade]) return grade;
15668
+ }
15669
+ return "E";
15670
+ }
15671
+ function capGrade(grade, cap) {
15672
+ return QUALITY_GRADES[Math.max(QUALITY_GRADES.indexOf(grade), QUALITY_GRADES.indexOf(cap))];
15673
+ }
15674
+ function isOpenFinding(finding) {
15675
+ return (finding.status ?? "open") === "open";
15676
+ }
15677
+ function resolveKritikLibrary(section, library) {
15678
+ const explicit = library ?? section?.library;
15679
+ if (explicit && Array.isArray(explicit.criteria) && Array.isArray(explicit.domains)) return explicit;
15680
+ if (!section) return void 0;
15681
+ const ids = /* @__PURE__ */ new Set();
15682
+ for (const row of [...asArray(section.assessments), ...asArray(section.findings)]) {
15683
+ if (typeof row?.criterion_id === "string" && row.criterion_id !== "") ids.add(row.criterion_id);
15684
+ }
15685
+ if (ids.size === 0) return void 0;
15686
+ const domainOf = (id) => id.includes("-") ? id.slice(0, id.lastIndexOf("-")) : id;
15687
+ const criteria = [...ids].sort().map((id) => ({ id, domain: domainOf(id), weight: 1 }));
15688
+ const domains = [...new Set(criteria.map((c) => c.domain))].sort().map((code) => ({ code, name: code }));
15689
+ return { version: explicit?.version ?? "unknown", domains, criteria, scales: explicit?.scales };
15690
+ }
15691
+ function deriveQualityMatrix(bundle, library) {
15692
+ const section = bundle.quality;
15693
+ const pack = resolveKritikLibrary(section, library);
15694
+ const assessments = asArray(section?.assessments);
15695
+ const findings = asArray(section?.findings);
15696
+ const openFindings = findings.filter((finding) => isOpenFinding(finding));
15697
+ const declared = asArray(section?.profile?.surfaces).map((surface) => surface?.id).filter((id) => typeof id === "string" && id !== "");
15698
+ const surfaces = declared.length > 0 ? [...new Set(declared)] : [...new Set(assessments.map((a) => a.surface).filter((id) => typeof id === "string" && id !== ""))].sort();
15699
+ const domains = asArray(pack?.domains).map((domain2) => domain2?.code).filter((code) => typeof code === "string" && code !== "");
15700
+ const domainOfCriterion = /* @__PURE__ */ new Map();
15701
+ const weightOfCriterion = /* @__PURE__ */ new Map();
15702
+ for (const criterion of asArray(pack?.criteria)) {
15703
+ if (typeof criterion?.id !== "string") continue;
15704
+ if (typeof criterion.domain === "string") domainOfCriterion.set(criterion.id, criterion.domain);
15705
+ weightOfCriterion.set(criterion.id, typeof criterion.weight === "number" && criterion.weight > 0 ? criterion.weight : 1);
15706
+ }
15707
+ const caps = capsOf(pack);
15708
+ const weights = section?.profile?.domain_weights;
15709
+ const matrix = {};
15710
+ for (const domain2 of domains) {
15711
+ matrix[domain2] = {};
15712
+ for (const surface of surfaces) {
15713
+ const rows = assessments.filter((a) => a.surface === surface && domainOfCriterion.get(a.criterion_id) === domain2);
15714
+ if (rows.length === 0) {
15715
+ matrix[domain2][surface] = null;
15716
+ continue;
15717
+ }
15718
+ let earned = 0;
15719
+ let available = 0;
15720
+ for (const row of rows) {
15721
+ const weight = weightOfCriterion.get(row.criterion_id) ?? 1;
15722
+ const level = typeof row.level === "number" ? Math.min(Math.max(row.level, 0), 4) : 0;
15723
+ earned += level * weight;
15724
+ available += 4 * weight;
15725
+ }
15726
+ if (available === 0) {
15727
+ matrix[domain2][surface] = null;
15728
+ continue;
15729
+ }
15730
+ const score = Math.round(earned / available * 100);
15731
+ const cellSeverities = openFindings.filter((finding) => finding.surface === surface && domainOfCriterion.get(finding.criterion_id) === domain2).map((finding) => severityOf(finding, pack));
15732
+ const banded = gradeOf(score, pack);
15733
+ let grade = banded;
15734
+ if (cellSeverities.includes("critical")) grade = capGrade(grade, caps.critical_open);
15735
+ else if (cellSeverities.includes("high")) grade = capGrade(grade, caps.high_open);
15736
+ matrix[domain2][surface] = {
15737
+ score,
15738
+ grade,
15739
+ capped: grade !== banded,
15740
+ criteria: rows.length,
15741
+ findings: {
15742
+ critical: cellSeverities.filter((s) => s === "critical").length,
15743
+ high: cellSeverities.filter((s) => s === "high").length,
15744
+ medium: cellSeverities.filter((s) => s === "medium").length,
15745
+ low: cellSeverities.filter((s) => s === "low").length
15746
+ }
15747
+ };
15748
+ }
15749
+ }
15750
+ const overall = {};
15751
+ for (const surface of surfaces) {
15752
+ let weighted = 0;
15753
+ let total = 0;
15754
+ for (const domain2 of domains) {
15755
+ const cell = matrix[domain2]?.[surface];
15756
+ if (!cell) continue;
15757
+ const weight = typeof weights?.[domain2] === "number" ? weights[domain2] : 1;
15758
+ weighted += cell.score * weight;
15759
+ total += weight;
15760
+ }
15761
+ overall[surface] = total > 0 ? Math.round(weighted / total) : null;
15762
+ }
15763
+ const finding_counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
15764
+ for (const finding of openFindings) finding_counts[severityOf(finding, pack)]++;
15765
+ return {
15766
+ framework_version: section?.framework_version,
15767
+ surfaces,
15768
+ domains,
15769
+ matrix,
15770
+ overall,
15771
+ finding_counts
15772
+ };
15773
+ }
15774
+
15385
15775
  // ../schema/src/validate.ts
15386
15776
  var VALID_STAGES = ["beta", "monitoring", "deprecated"];
15387
15777
  var VALID_VIEW_CARD_VARIANTS = ["compact", "large"];
@@ -15898,13 +16288,98 @@ function validateBundle(input) {
15898
16288
  composesSet.add(`${sourceId}->${targetId}`);
15899
16289
  }
15900
16290
  });
16291
+ const PLAYLIST_ENTRY_TYPES = ["view", "flow", "condition", "junction"];
16292
+ const checkPlaylistShape = (entries, flowId, path6, depth = 0) => {
16293
+ if (depth > 50) return;
16294
+ if (!Array.isArray(entries)) {
16295
+ error51(path6, "playlist-entry-shape", `Flow ${flowId}: ${path6} must be an array of playlist entries`);
16296
+ return;
16297
+ }
16298
+ entries.forEach((raw, i) => {
16299
+ const p = `${path6}[${i}]`;
16300
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
16301
+ error51(p, "playlist-entry-shape", `Flow ${flowId}: ${p} must be a playlist entry object`);
16302
+ return;
16303
+ }
16304
+ const entry = raw;
16305
+ const type = entry.type;
16306
+ if (typeof type !== "string" || !PLAYLIST_ENTRY_TYPES.includes(type)) {
16307
+ error51(
16308
+ `${p}.type`,
16309
+ "playlist-entry-shape",
16310
+ `Flow ${flowId}: ${p}.type must be one of ${PLAYLIST_ENTRY_TYPES.join(", ")} (got ${JSON.stringify(type)})`
16311
+ );
16312
+ return;
16313
+ }
16314
+ if (type === "view") {
16315
+ if (typeof entry.view_id !== "string") {
16316
+ error51(`${p}.view_id`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a view entry with no view_id string`);
16317
+ }
16318
+ } else if (type === "flow") {
16319
+ if (typeof entry.flow_id !== "string") {
16320
+ error51(`${p}.flow_id`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a flow entry with no flow_id string`);
16321
+ }
16322
+ } else if (type === "condition") {
16323
+ if (typeof entry.label !== "string") {
16324
+ error51(`${p}.label`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a condition with no label string`);
16325
+ }
16326
+ for (const branch of ["if_true", "if_false"]) {
16327
+ if (!Array.isArray(entry[branch])) {
16328
+ error51(
16329
+ `${p}.${branch}`,
16330
+ "playlist-entry-shape",
16331
+ `Flow ${flowId}: ${p}.${branch} must be an array of playlist entries (both branches are required, use [] for an empty one)`
16332
+ );
16333
+ } else {
16334
+ checkPlaylistShape(entry[branch], flowId, `${p}.${branch}`, depth + 1);
16335
+ }
16336
+ }
16337
+ } else {
16338
+ if (typeof entry.label !== "string") {
16339
+ error51(`${p}.label`, "playlist-entry-shape", `Flow ${flowId}: ${p} is a junction with no label string`);
16340
+ }
16341
+ if (!Array.isArray(entry.cases)) {
16342
+ error51(`${p}.cases`, "playlist-entry-shape", `Flow ${flowId}: ${p}.cases must be an array of junction cases`);
16343
+ return;
16344
+ }
16345
+ entry.cases.forEach((rawCase, j) => {
16346
+ const cp = `${p}.cases[${j}]`;
16347
+ if (typeof rawCase !== "object" || rawCase === null || Array.isArray(rawCase)) {
16348
+ error51(cp, "playlist-entry-shape", `Flow ${flowId}: ${cp} must be a junction case object`);
16349
+ return;
16350
+ }
16351
+ const branch = rawCase;
16352
+ if (typeof branch.label !== "string") {
16353
+ error51(`${cp}.label`, "playlist-entry-shape", `Flow ${flowId}: ${cp} has no label string`);
16354
+ }
16355
+ if (!Array.isArray(branch.entries)) {
16356
+ error51(
16357
+ `${cp}.entries`,
16358
+ "playlist-entry-shape",
16359
+ `Flow ${flowId}: ${cp}.entries must be an array of playlist entries \u2014 a junction case holds its own entries, not a bare view_id/flow_id`
16360
+ );
16361
+ } else {
16362
+ checkPlaylistShape(branch.entries, flowId, `${cp}.entries`, depth + 1);
16363
+ }
16364
+ });
16365
+ }
16366
+ });
16367
+ };
16368
+ nodes.forEach((node, i) => {
16369
+ const md = node.metadata;
16370
+ if (node.species === "flow" && md?.playlist?.entries) {
16371
+ checkPlaylistShape(md.playlist.entries, node.id, `nodes[${i}].metadata.playlist.entries`);
16372
+ }
16373
+ });
15901
16374
  const collectPlaylistRefs = (entries, flowId, path6, depth = 0) => {
15902
16375
  if (depth > 50) {
15903
16376
  error51(path6, "playlist-depth", `Flow ${flowId}: playlist nesting too deep (possible cycle)`);
15904
16377
  return [];
15905
16378
  }
15906
16379
  const refs = [];
16380
+ if (!Array.isArray(entries)) return refs;
15907
16381
  for (const entry of entries) {
16382
+ if (typeof entry !== "object" || entry === null) continue;
15908
16383
  if (entry.type === "view") {
15909
16384
  if (!nodeIds.has(entry.view_id)) {
15910
16385
  error51(path6, "playlist-ref-exists", `Flow ${flowId}: playlist references non-existent view "${entry.view_id}"`);
@@ -15956,7 +16431,9 @@ function validateBundle(input) {
15956
16431
  const nodeId = node.id;
15957
16432
  const subFlows = [];
15958
16433
  const findSubFlows = (entries) => {
16434
+ if (!Array.isArray(entries)) return;
15959
16435
  for (const e of entries) {
16436
+ if (typeof e !== "object" || e === null) continue;
15960
16437
  if (e.type === "flow") subFlows.push(e.flow_id);
15961
16438
  if (e.type === "condition") {
15962
16439
  if (e.if_true) findSubFlows(e.if_true);
@@ -15991,6 +16468,151 @@ function validateBundle(input) {
15991
16468
  for (const id of flowGraph.keys()) {
15992
16469
  dfs(id);
15993
16470
  }
16471
+ const quality = bundle.quality;
16472
+ if (typeof quality === "object" && quality !== null && !Array.isArray(quality)) {
16473
+ const section = quality;
16474
+ const assessments = Array.isArray(section.assessments) ? section.assessments : [];
16475
+ const qualityFindings = Array.isArray(section.findings) ? section.findings : [];
16476
+ const profile = typeof section.profile === "object" && section.profile !== null ? section.profile : void 0;
16477
+ const declaredSurfaces = /* @__PURE__ */ new Set();
16478
+ if (Array.isArray(profile?.surfaces)) {
16479
+ profile.surfaces.forEach((entry, index) => {
16480
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
16481
+ const surface = entry;
16482
+ const surfaceId = typeof surface.id === "string" ? surface.id : void 0;
16483
+ if (surfaceId === void 0 || surfaceId.trim() === "") return;
16484
+ if (declaredSurfaces.has(surfaceId)) {
16485
+ warn(`quality.profile.surfaces[${index}].id`, "quality-duplicate-surface", `Duplicate surface id "${surfaceId}" \u2014 the first wins`);
16486
+ }
16487
+ declaredSurfaces.add(surfaceId);
16488
+ });
16489
+ }
16490
+ if (declaredSurfaces.size === 0 && (assessments.length > 0 || qualityFindings.length > 0)) {
16491
+ warn(
16492
+ "quality.profile.surfaces",
16493
+ "quality-no-surfaces",
16494
+ "Quality data is stored but the profile declares no surfaces \u2014 the matrix falls back to the surfaces the assessments name"
16495
+ );
16496
+ }
16497
+ const library = typeof section.library === "object" && section.library !== null ? section.library : void 0;
16498
+ const knownCriteria = /* @__PURE__ */ new Set();
16499
+ const retiredCriteria = /* @__PURE__ */ new Map();
16500
+ if (Array.isArray(library?.criteria)) {
16501
+ for (const entry of library.criteria) {
16502
+ if (typeof entry !== "object" || entry === null) continue;
16503
+ const criterion = entry;
16504
+ if (typeof criterion.id !== "string") continue;
16505
+ knownCriteria.add(criterion.id);
16506
+ if (typeof criterion.superseded_by === "string" && criterion.superseded_by !== "") {
16507
+ retiredCriteria.set(criterion.id, criterion.superseded_by);
16508
+ }
16509
+ }
16510
+ } else if (assessments.length > 0 || qualityFindings.length > 0) {
16511
+ warn(
16512
+ "quality.library",
16513
+ "quality-library-missing",
16514
+ "No criteria pack is embedded \u2014 criterion ids cannot be resolved to domains or weights; the matrix uses a synthesized library"
16515
+ );
16516
+ }
16517
+ if (typeof section.framework_version !== "string" || section.framework_version === "") {
16518
+ warn("quality.framework_version", "quality-framework-version-missing", "Quality section has no framework_version \u2014 scores are not comparable across audits without one");
16519
+ }
16520
+ const checkRowRefs = (row, path6, label) => {
16521
+ const criterionId = typeof row.criterion_id === "string" ? row.criterion_id : void 0;
16522
+ if (criterionId !== void 0 && knownCriteria.size > 0 && !knownCriteria.has(criterionId)) {
16523
+ warn(`${path6}.criterion_id`, "quality-unknown-criterion", `${label}: criterion "${criterionId}" is not in the pinned library`);
16524
+ }
16525
+ const supersededBy = criterionId !== void 0 ? retiredCriteria.get(criterionId) : void 0;
16526
+ if (supersededBy !== void 0) {
16527
+ warn(`${path6}.criterion_id`, "quality-retired-criterion", `${label}: criterion "${criterionId}" is retired \u2014 superseded by "${supersededBy}"`);
16528
+ }
16529
+ const surface = typeof row.surface === "string" ? row.surface : void 0;
16530
+ if (surface !== void 0 && surface !== CROSS_SURFACE_ID && declaredSurfaces.size > 0 && !declaredSurfaces.has(surface)) {
16531
+ warn(`${path6}.surface`, "quality-unknown-surface", `${label}: surface "${surface}" is not declared in the profile`);
16532
+ }
16533
+ };
16534
+ const seenCells = /* @__PURE__ */ new Set();
16535
+ assessments.forEach((entry, index) => {
16536
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
16537
+ const assessment = entry;
16538
+ const path6 = `quality.assessments[${index}]`;
16539
+ const label = `Assessment ${index}`;
16540
+ checkRowRefs(assessment, path6, label);
16541
+ const level = assessment.level;
16542
+ if (typeof level !== "number" || !Number.isInteger(level) || level < 0 || level > 4) {
16543
+ warn(`${path6}.level`, "quality-level-range", `${label}: level must be an integer 0-4 (N/A is the absence of the row, not a level)`);
16544
+ }
16545
+ if (assessment.surface === CROSS_SURFACE_ID) {
16546
+ warn(`${path6}.surface`, "quality-cross-surface-assessment", `${label}: "${CROSS_SURFACE_ID}" is a findings-only lens \u2014 it carries no matrix column, so this score would render nowhere`);
16547
+ }
16548
+ if (typeof assessment.evidence !== "string" || assessment.evidence.trim() === "") {
16549
+ warn(`${path6}.evidence`, "quality-assessment-no-evidence", `${label}: scored with no evidence \u2014 a score without a citation is an opinion, not an assessment`);
16550
+ }
16551
+ const criterionId = typeof assessment.criterion_id === "string" ? assessment.criterion_id : void 0;
16552
+ const surface = typeof assessment.surface === "string" ? assessment.surface : void 0;
16553
+ if (criterionId !== void 0 && surface !== void 0) {
16554
+ const cell = `${criterionId}\0${surface}`;
16555
+ if (seenCells.has(cell)) {
16556
+ warn(`${path6}`, "quality-duplicate-assessment", `${label}: a second score for (${criterionId} x ${surface}) \u2014 the section stores latest-per-cell, so this cell is ambiguous`);
16557
+ }
16558
+ seenCells.add(cell);
16559
+ }
16560
+ });
16561
+ const seenFindingIds = /* @__PURE__ */ new Set();
16562
+ qualityFindings.forEach((entry, index) => {
16563
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
16564
+ const finding = entry;
16565
+ const path6 = `quality.findings[${index}]`;
16566
+ const label = `Finding ${index}`;
16567
+ checkRowRefs(finding, path6, label);
16568
+ const findingId = typeof finding.id === "string" ? finding.id : void 0;
16569
+ if (findingId !== void 0) {
16570
+ if (seenFindingIds.has(findingId)) {
16571
+ warn(`${path6}.id`, "quality-duplicate-finding-id", `Duplicate finding id "${findingId}"`);
16572
+ }
16573
+ seenFindingIds.add(findingId);
16574
+ }
16575
+ for (const field of ["impact", "likelihood"]) {
16576
+ const value = finding[field];
16577
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 5) {
16578
+ warn(`${path6}.${field}`, "quality-risk-range", `${label}: ${field} must be an integer 1-5 \u2014 severity is derived from impact x likelihood`);
16579
+ }
16580
+ }
16581
+ for (const derived of ["severity", "priority"]) {
16582
+ if (derived in finding) {
16583
+ warn(`${path6}.${derived}`, "quality-derived-field-stored", `${label}: ${derived} is derived from impact, likelihood and cost \u2014 storing it lets it drift from the numbers behind it`);
16584
+ }
16585
+ }
16586
+ if (finding.status === "accepted-risk" && (typeof finding.detail !== "string" || finding.detail.trim() === "")) {
16587
+ warn(`${path6}.detail`, "quality-accepted-risk-no-note", `${label}: accepted-risk with no note \u2014 an accepted risk is a decision and reads like one`);
16588
+ }
16589
+ });
16590
+ const openedInJournal = /* @__PURE__ */ new Set();
16591
+ const journalEvents = Array.isArray(bundle.journal) ? bundle.journal : [];
16592
+ journalEvents.forEach((entry) => {
16593
+ if (typeof entry !== "object" || entry === null) return;
16594
+ const event = entry;
16595
+ if (event.type === "quality.finding.opened" && typeof event.finding_id === "string") openedInJournal.add(event.finding_id);
16596
+ });
16597
+ journalEvents.forEach((entry, index) => {
16598
+ if (typeof entry !== "object" || entry === null) return;
16599
+ const event = entry;
16600
+ const type = typeof event.type === "string" ? event.type : "";
16601
+ if (!type.startsWith("quality.")) return;
16602
+ if (typeof event.actor !== "string" || event.actor.trim() === "") {
16603
+ warn(`journal[${index}].actor`, "quality-event-no-actor", `Journal event ${index}: ${type} has no actor \u2014 human, agent and CI scores become indistinguishable`);
16604
+ }
16605
+ if (type === "quality.finding.resolved" && typeof event.finding_id === "string") {
16606
+ if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
16607
+ warn(
16608
+ `journal[${index}].finding_id`,
16609
+ "quality-resolved-never-opened",
16610
+ `Journal event ${index}: resolves finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
16611
+ );
16612
+ }
16613
+ }
16614
+ });
16615
+ }
15994
16616
  for (const finding of crossCheckJournal(bundle)) findings.push(finding);
15995
16617
  return result();
15996
16618
  }
@@ -16379,6 +17001,292 @@ function promotionPatch(node, promotion) {
16379
17001
  };
16380
17002
  }
16381
17003
 
17004
+ // ../schema/src/quality-ops.ts
17005
+ var DEFAULT_TARGET_LEVEL = 3;
17006
+ function upsertAssessment(assessments, next) {
17007
+ const index = assessments.findIndex(
17008
+ (candidate) => candidate.criterion_id === next.criterion_id && candidate.surface === next.surface
17009
+ );
17010
+ if (index === -1) return { assessments: [...assessments, next] };
17011
+ const updated = [...assessments];
17012
+ const replaced = updated[index];
17013
+ updated[index] = next;
17014
+ return { assessments: updated, replaced };
17015
+ }
17016
+ function parseSurfaceSpec(spec) {
17017
+ const parts = spec.split(":");
17018
+ const id = (parts[0] ?? "").trim();
17019
+ if (id === "") throw new Error(`--surface needs an id (got "${spec}")`);
17020
+ if (id === CROSS_SURFACE_ID) {
17021
+ throw new Error(
17022
+ `"${CROSS_SURFACE_ID}" is reserved \u2014 it is the contract lens between surfaces. Findings may carry it; it holds no assessments and never becomes a matrix column, so it is not declared here.`
17023
+ );
17024
+ }
17025
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(id)) {
17026
+ throw new Error(
17027
+ `surface id "${id}" is not kebab-case \u2014 assessments reference it, so it has to be stable and typo-proof`
17028
+ );
17029
+ }
17030
+ const title2 = (parts[1] ?? "").trim() || id;
17031
+ const platform = (parts[2] ?? "").trim();
17032
+ if (platform !== "" && !PLATFORM_IDS.includes(platform)) {
17033
+ throw new Error(
17034
+ `"${platform}" is not an Arkaik platform (${PLATFORM_IDS.join(", ")}). A surface that ships no views simply has none \u2014 leave it off rather than inventing one.`
17035
+ );
17036
+ }
17037
+ return platform === "" ? { id, title: title2 } : { id, title: title2, platform };
17038
+ }
17039
+ function domainCodeOf(criterionId, library) {
17040
+ const criterion = (library?.criteria ?? []).find((candidate) => candidate.id === criterionId);
17041
+ if (typeof criterion?.domain === "string" && criterion.domain !== "") return criterion.domain;
17042
+ const dash = criterionId.indexOf("-");
17043
+ return dash > 0 ? criterionId.slice(0, dash) : criterionId;
17044
+ }
17045
+ function mintFindingId(auditId, criterionId, surface, taken, library) {
17046
+ const prefix = `F-${auditId}-${domainCodeOf(criterionId, library)}-${surface}-`;
17047
+ const used = new Set(taken);
17048
+ for (let n = 1; n < 1e3; n++) {
17049
+ const candidate = `${prefix}${String(n).padStart(2, "0")}`;
17050
+ if (!used.has(candidate)) return candidate;
17051
+ }
17052
+ throw new Error(`Kritik: exhausted finding ids for ${prefix}NN \u2014 999 findings on one cell is a data problem, not a numbering one`);
17053
+ }
17054
+ function upsertFinding(findings, next) {
17055
+ const index = findings.findIndex((candidate) => candidate.id === next.id);
17056
+ if (index === -1) return { findings: [...findings, next] };
17057
+ const updated = [...findings];
17058
+ const replaced = updated[index];
17059
+ updated[index] = next;
17060
+ return { findings: updated, replaced };
17061
+ }
17062
+ function patchFinding(findings, id, patch) {
17063
+ const index = findings.findIndex((candidate) => candidate.id === id);
17064
+ if (index === -1) return { findings: [...findings] };
17065
+ const updated = [...findings];
17066
+ const previous = updated[index];
17067
+ const finding = { ...previous, ...patch };
17068
+ updated[index] = finding;
17069
+ return { findings: updated, finding, previous };
17070
+ }
17071
+ function resolveFinding(findings, id, resolvedBy) {
17072
+ return patchFinding(findings, id, {
17073
+ status: "resolved",
17074
+ ...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {}
17075
+ });
17076
+ }
17077
+ function acceptFinding(findings, id, note) {
17078
+ const existing = findings.find((candidate) => candidate.id === id);
17079
+ const detail = existing === void 0 ? note : `${existing.detail}
17080
+
17081
+ Accepted risk: ${note}`.trim();
17082
+ return patchFinding(findings, id, { status: "accepted-risk", detail });
17083
+ }
17084
+ function fillTemplate(text, values) {
17085
+ return text.replace(/\{(\w+)\}/g, (whole, key) => values[key] ?? whole);
17086
+ }
17087
+ function renderIssue(criterion, options) {
17088
+ const { surface, finding, library } = options;
17089
+ const anchors = criterion.level_anchors ?? {};
17090
+ const level = options.level === void 0 || options.level === "" ? "" : String(options.level);
17091
+ const observed = level === "" ? "{observed_level}" : level;
17092
+ const target = options.targetLevel !== void 0 && options.targetLevel !== "" ? String(options.targetLevel) : level === "" ? "{target_level}" : String(Math.min(Number(level) + 1, 4));
17093
+ const values = {
17094
+ surface,
17095
+ observed_level: observed,
17096
+ target_level: target,
17097
+ observed_level_name: anchors[`l${observed}`] ?? "{observed_level_name}",
17098
+ target_level_name: anchors[`l${target}`] ?? "{target_level_name}",
17099
+ target_anchor_text: anchors[`l${target}`] ?? "{target_anchor_text}",
17100
+ impact: String(finding?.impact ?? criterion.default_impact ?? 3)
17101
+ };
17102
+ if (finding !== void 0) {
17103
+ values.likelihood = String(finding.likelihood);
17104
+ values.evidence_bullets_with_file_paths = finding.evidence;
17105
+ values.risk_narrative = finding.detail;
17106
+ if (typeof finding.remediation === "string" && finding.remediation !== "") {
17107
+ values.remediation_step_1 = finding.remediation;
17108
+ }
17109
+ }
17110
+ const issue2 = criterion.issue ?? {};
17111
+ const labels = [.../* @__PURE__ */ new Set([...issue2.labels ?? ["quality"], surface])];
17112
+ const body = fillTemplate(issue2.body_skeleton ?? "", values);
17113
+ return {
17114
+ title: fillTemplate(issue2.title_template ?? `[Quality] ${criterion.id} on {surface}`, values),
17115
+ labels,
17116
+ body: finding === void 0 ? body : `${body}
17117
+ ${findingFooter(finding, library)}`,
17118
+ ...typeof criterion.remediation === "string" && criterion.remediation !== "" ? { remediation: criterion.remediation } : {}
17119
+ };
17120
+ }
17121
+ function findingFooter(finding, library) {
17122
+ const lines = [
17123
+ ``,
17124
+ `---`,
17125
+ `<sub>Kritik finding \`${finding.id}\` \u2014 **${severityOf(finding, library)} / ${priorityOf(finding, library)}** (impact ${finding.impact} x likelihood ${finding.likelihood}, cost ${finding.cost})</sub>`,
17126
+ ``,
17127
+ `**Evidence**`,
17128
+ finding.evidence
17129
+ ];
17130
+ if (typeof finding.remediation === "string" && finding.remediation !== "") {
17131
+ lines.push(``, `**Remediation**`, finding.remediation);
17132
+ }
17133
+ return lines.join("\n");
17134
+ }
17135
+ function signalRunSheet(library, surfaces, filter = {}) {
17136
+ const surfaceIds = surfaces.map((surface) => surface.id).filter((id) => id !== CROSS_SURFACE_ID);
17137
+ const rows = [];
17138
+ for (const criterion of library.criteria ?? []) {
17139
+ if (criterion.superseded_by !== void 0) continue;
17140
+ if (filter.criterion !== void 0 && criterion.id !== filter.criterion) continue;
17141
+ if (filter.domain !== void 0 && criterion.domain !== filter.domain) continue;
17142
+ const signals = criterion.signals ?? [];
17143
+ if (signals.length === 0) continue;
17144
+ const appliesTo = criterion.applies_to;
17145
+ for (const surfaceId of surfaceIds) {
17146
+ if (Array.isArray(appliesTo) && !appliesTo.includes(surfaceId)) continue;
17147
+ if (filter.surface !== void 0 && surfaceId !== filter.surface) continue;
17148
+ signals.forEach((signal, index) => {
17149
+ rows.push({ criterion_id: criterion.id, surface: surfaceId, index, signal });
17150
+ });
17151
+ }
17152
+ }
17153
+ return rows;
17154
+ }
17155
+ function auditCompletedInput(matrix, meta3) {
17156
+ const scores = {};
17157
+ for (const surface of matrix.surfaces) {
17158
+ const bySurface = {};
17159
+ for (const domain2 of matrix.domains) {
17160
+ const cell = matrix.matrix[domain2]?.[surface];
17161
+ if (cell) bySurface[domain2] = cell.score;
17162
+ }
17163
+ scores[surface] = bySurface;
17164
+ }
17165
+ return {
17166
+ type: "quality.audit.completed",
17167
+ payload: {
17168
+ audit_id: meta3.audit_id,
17169
+ framework_version: meta3.framework_version,
17170
+ ...meta3.commit !== void 0 ? { commit: meta3.commit } : {},
17171
+ scores,
17172
+ counts: matrix.finding_counts
17173
+ }
17174
+ };
17175
+ }
17176
+ function findingOpenedInput(finding, library) {
17177
+ return {
17178
+ type: "quality.finding.opened",
17179
+ payload: {
17180
+ finding_id: finding.id,
17181
+ criterion_id: finding.criterion_id,
17182
+ surface: finding.surface,
17183
+ severity: severityOf(finding, library),
17184
+ priority: priorityOf(finding, library),
17185
+ title: finding.title,
17186
+ ...finding.node_ids !== void 0 ? { node_ids: finding.node_ids } : {},
17187
+ ...finding.issue_url !== void 0 ? { issue_url: finding.issue_url } : {}
17188
+ }
17189
+ };
17190
+ }
17191
+ function findingResolvedInput(finding, resolvedBy) {
17192
+ return {
17193
+ type: "quality.finding.resolved",
17194
+ payload: {
17195
+ finding_id: finding.id,
17196
+ ...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {},
17197
+ ...finding.node_ids !== void 0 ? { node_ids: finding.node_ids } : {}
17198
+ }
17199
+ };
17200
+ }
17201
+ function signalTrippedInput(trip) {
17202
+ return {
17203
+ type: "quality.signal.tripped",
17204
+ payload: {
17205
+ criterion_id: trip.criterion_id,
17206
+ surface: trip.surface,
17207
+ signal: trip.signal,
17208
+ ...trip.detail !== void 0 ? { detail: trip.detail } : {}
17209
+ }
17210
+ };
17211
+ }
17212
+
17213
+ // ../schema/src/quality-regressions.ts
17214
+ var REGRESSION_SIGNALS = {
17215
+ "level-drop": "maturity on this cell does not regress",
17216
+ "new-severe-finding": "no open Critical or High finding on this cell",
17217
+ "reopened-finding": "a resolved finding stays resolved"
17218
+ };
17219
+ var cellKey = (criterionId, surface) => `${criterionId}::${surface}`;
17220
+ var rowsOf = (value) => Array.isArray(value) ? value : [];
17221
+ function assessmentsByCell(state) {
17222
+ const cells = /* @__PURE__ */ new Map();
17223
+ for (const assessment of rowsOf(state?.assessments)) {
17224
+ if (typeof assessment?.criterion_id !== "string" || typeof assessment?.surface !== "string") continue;
17225
+ cells.set(cellKey(assessment.criterion_id, assessment.surface), assessment);
17226
+ }
17227
+ return cells;
17228
+ }
17229
+ function severeByCell(state, library) {
17230
+ const cells = /* @__PURE__ */ new Map();
17231
+ for (const finding of rowsOf(state?.findings)) {
17232
+ if (typeof finding?.criterion_id !== "string" || typeof finding?.surface !== "string") continue;
17233
+ if (!isOpenFinding(finding)) continue;
17234
+ const severity = severityOf(finding, library);
17235
+ if (severity !== "critical" && severity !== "high") continue;
17236
+ const key = cellKey(finding.criterion_id, finding.surface);
17237
+ const existing = cells.get(key);
17238
+ if (existing === void 0) cells.set(key, [finding]);
17239
+ else existing.push(finding);
17240
+ }
17241
+ return cells;
17242
+ }
17243
+ function detectRegressions(previous, next, library) {
17244
+ const before = assessmentsByCell(previous);
17245
+ const after = assessmentsByCell(next);
17246
+ const comparable = (key) => before.has(key) && after.has(key);
17247
+ const regressions = [];
17248
+ for (const [key, current] of after) {
17249
+ const earlier = before.get(key);
17250
+ if (earlier === void 0) continue;
17251
+ if (!(Number(current.level) < Number(earlier.level))) continue;
17252
+ regressions.push({
17253
+ kind: "level-drop",
17254
+ criterion_id: current.criterion_id,
17255
+ surface: current.surface,
17256
+ signal: REGRESSION_SIGNALS["level-drop"],
17257
+ detail: `level ${earlier.level} \u2192 ${current.level} (${earlier.audit_id} \u2192 ${current.audit_id})`
17258
+ });
17259
+ }
17260
+ const severeBefore = severeByCell(previous, library);
17261
+ for (const [key, findings] of severeByCell(next, library)) {
17262
+ if (!comparable(key)) continue;
17263
+ if ((severeBefore.get(key) ?? []).length > 0) continue;
17264
+ for (const finding of findings) {
17265
+ regressions.push({
17266
+ kind: "new-severe-finding",
17267
+ criterion_id: finding.criterion_id,
17268
+ surface: finding.surface,
17269
+ signal: REGRESSION_SIGNALS["new-severe-finding"],
17270
+ detail: `${finding.id} \u2014 ${severityOf(finding, library)} (impact ${finding.impact} \xD7 likelihood ${finding.likelihood})`
17271
+ });
17272
+ }
17273
+ }
17274
+ const resolvedBefore = new Set(
17275
+ rowsOf(previous?.findings).filter((finding) => finding?.status === "resolved").map((finding) => finding.id)
17276
+ );
17277
+ for (const finding of rowsOf(next?.findings)) {
17278
+ if (!isOpenFinding(finding) || !resolvedBefore.has(finding.id)) continue;
17279
+ regressions.push({
17280
+ kind: "reopened-finding",
17281
+ criterion_id: finding.criterion_id,
17282
+ surface: finding.surface,
17283
+ signal: REGRESSION_SIGNALS["reopened-finding"],
17284
+ detail: `${finding.id} \u2014 "${finding.title}"`
17285
+ });
17286
+ }
17287
+ return regressions;
17288
+ }
17289
+
16382
17290
  // src/commands/init.ts
16383
17291
  var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
16384
17292
  var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
@@ -16672,7 +17580,7 @@ function runInit(args) {
16672
17580
  }
16673
17581
 
16674
17582
  // src/commands/validate.ts
16675
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
17583
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
16676
17584
 
16677
17585
  // src/lib/bundle-io.ts
16678
17586
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -16703,9 +17611,80 @@ function nodesByIdOf(bundle) {
16703
17611
  }
16704
17612
 
16705
17613
  // src/lib/bundle-validate.ts
16706
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
17614
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
17615
+ import { basename as basename2 } from "node:path";
17616
+
17617
+ // src/lib/journal-io.ts
17618
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
16707
17619
  import { dirname as dirname2, join as join2 } from "node:path";
16708
17620
  var JOURNAL_SIDECAR = "journal.jsonl";
17621
+ function journalPathFor(bundlePath) {
17622
+ return join2(dirname2(bundlePath), JOURNAL_SIDECAR);
17623
+ }
17624
+ function archivePathFor(journalPath, version2) {
17625
+ return join2(dirname2(journalPath), "journal", `archive-${version2}.jsonl`);
17626
+ }
17627
+ function archivePathsFor(journalPath) {
17628
+ const dir = join2(dirname2(journalPath), "journal");
17629
+ if (!existsSync3(dir)) return [];
17630
+ return readdirSync(dir).filter((name) => name.startsWith("archive-") && name.endsWith(".jsonl")).sort().map((name) => join2(dir, name));
17631
+ }
17632
+ function readJournalEvents(journalPath) {
17633
+ if (!existsSync3(journalPath)) return [];
17634
+ return parseJournalLines(readFileSync3(journalPath, "utf8")).events;
17635
+ }
17636
+ function readFullJournalEvents(journalPath) {
17637
+ const events = readJournalEvents(journalPath);
17638
+ for (const archivePath of archivePathsFor(journalPath)) {
17639
+ events.push(...readJournalEvents(archivePath));
17640
+ }
17641
+ return events;
17642
+ }
17643
+ function loadJournalEvents(bundle, bundlePath) {
17644
+ if (Array.isArray(bundle.journal)) return bundle.journal;
17645
+ return readJournalEvents(journalPathFor(bundlePath));
17646
+ }
17647
+ function toLine(event) {
17648
+ return JSON.stringify(event) + "\n";
17649
+ }
17650
+ function appendJournalEvent(journalPath, event) {
17651
+ const line2 = toLine(event);
17652
+ if (!existsSync3(journalPath)) {
17653
+ mkdirSync2(dirname2(journalPath), { recursive: true });
17654
+ writeFileSync2(journalPath, line2);
17655
+ return;
17656
+ }
17657
+ const existing = readFileSync3(journalPath, "utf8");
17658
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
17659
+ appendFileSync(journalPath, prefix + line2);
17660
+ }
17661
+ function ensureJournalBaseline(journalPath, bundle, actor) {
17662
+ const snapshotNodeIds = (Array.isArray(bundle.nodes) ? bundle.nodes : []).map((node) => node?.id).filter((id) => typeof id === "string");
17663
+ const missing = missingProvenanceNodeIds(snapshotNodeIds, readFullJournalEvents(journalPath));
17664
+ if (missing.length === 0) return void 0;
17665
+ const event = makeEvent("journal.baseline", { node_ids: missing }, { actor });
17666
+ appendJournalEvent(journalPath, event);
17667
+ return event;
17668
+ }
17669
+ function compactSlice(journalPath, slice, version2) {
17670
+ if (slice.length === 0) return;
17671
+ const sliceIds = new Set(slice.map((ev) => ev.id));
17672
+ const all = readJournalEvents(journalPath);
17673
+ const surviving = all.filter((ev) => !sliceIds.has(ev.id));
17674
+ const archivePath = archivePathFor(journalPath, version2);
17675
+ mkdirSync2(dirname2(archivePath), { recursive: true });
17676
+ const archiveLines = slice.map(toLine).join("");
17677
+ if (existsSync3(archivePath)) {
17678
+ const existing = readFileSync3(archivePath, "utf8");
17679
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
17680
+ appendFileSync(archivePath, prefix + archiveLines);
17681
+ } else {
17682
+ writeFileSync2(archivePath, archiveLines);
17683
+ }
17684
+ writeFileSync2(journalPath, surviving.map(toLine).join(""));
17685
+ }
17686
+
17687
+ // src/lib/bundle-validate.ts
16709
17688
  function validateBundleAt(filePath) {
16710
17689
  const bundle = readBundle(filePath);
16711
17690
  const loose = bundle;
@@ -16714,21 +17693,49 @@ function validateBundleAt(filePath) {
16714
17693
  }
16715
17694
  let sidecarFindings = [];
16716
17695
  let sidecarLoaded = false;
17696
+ const archiveFindings = [];
17697
+ const archivesLoaded = [];
16717
17698
  if (loose.journal === void 0) {
16718
- const sidecarPath = join2(dirname2(filePath), JOURNAL_SIDECAR);
16719
- if (existsSync3(sidecarPath)) {
16720
- const { events, findings } = parseJournalLines(readFileSync3(sidecarPath, "utf8"));
17699
+ const sidecarPath = journalPathFor(filePath);
17700
+ const folded = [];
17701
+ if (existsSync4(sidecarPath)) {
17702
+ const { events, findings } = parseJournalLines(readFileSync4(sidecarPath, "utf8"));
16721
17703
  sidecarFindings = findings;
16722
17704
  sidecarLoaded = true;
16723
- loose.journal = events;
17705
+ folded.push(...events);
16724
17706
  }
17707
+ for (const archivePath of archivePathsFor(sidecarPath)) {
17708
+ const { events, findings } = parseJournalLines(readFileSync4(archivePath, "utf8"));
17709
+ const file2 = basename2(archivePath);
17710
+ for (const finding of findings) archiveFindings.push({ ...finding, file: file2 });
17711
+ archivesLoaded.push(file2);
17712
+ folded.push(...events);
17713
+ }
17714
+ if (sidecarLoaded || archivesLoaded.length > 0) loose.journal = folded;
16725
17715
  }
16726
17716
  const nodes = Array.isArray(loose.nodes) ? loose.nodes : [];
16727
17717
  const edges = Array.isArray(loose.edges) ? loose.edges : [];
16728
17718
  const journal = Array.isArray(loose.journal) ? loose.journal : [];
16729
17719
  const result = validateBundle(bundle);
16730
- const valid = sidecarFindings.length === 0 && result.errors.length === 0;
16731
- return { bundle, nodes, edges, journal, sidecarLoaded, sidecarFindings, result, valid };
17720
+ const valid = sidecarFindings.length === 0 && archiveFindings.length === 0 && result.errors.length === 0;
17721
+ return {
17722
+ bundle,
17723
+ nodes,
17724
+ edges,
17725
+ journal,
17726
+ sidecarLoaded,
17727
+ sidecarFindings,
17728
+ archivesLoaded,
17729
+ archiveFindings,
17730
+ result,
17731
+ valid
17732
+ };
17733
+ }
17734
+ function journalLineErrorLines(v) {
17735
+ return [
17736
+ ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
17737
+ ...v.archiveFindings.map((f) => `ERROR [${f.rule}] ${f.file} line ${f.line}: ${f.message}`)
17738
+ ];
16732
17739
  }
16733
17740
 
16734
17741
  // src/commands/validate.ts
@@ -16765,12 +17772,12 @@ function fixFormat(filePath) {
16765
17772
  } catch (e) {
16766
17773
  return fail2(`FATAL: ${e.message}`);
16767
17774
  }
16768
- const before = readFileSync4(filePath, "utf8");
17775
+ const before = readFileSync5(filePath, "utf8");
16769
17776
  const after = serializeBundle(bundle);
16770
17777
  if (after === before) {
16771
17778
  console.log(`Already canonical: ${filePath}`);
16772
17779
  } else {
16773
- writeFileSync2(filePath, after);
17780
+ writeFileSync3(filePath, after);
16774
17781
  console.log(`Reformatted: ${filePath}`);
16775
17782
  }
16776
17783
  process.exit(0);
@@ -16789,8 +17796,12 @@ function validate(filePath) {
16789
17796
  ` Nodes: ${v.nodes.length} (${countBySpecies(v.nodes, "view")} views, ${countBySpecies(v.nodes, "flow")} flows, ${countBySpecies(v.nodes, "data-model")} data-models, ${countBySpecies(v.nodes, "api-endpoint")} api-endpoints)`
16790
17797
  );
16791
17798
  console.log(` Edges: ${v.edges.length}`);
16792
- if (v.sidecarLoaded) {
16793
- console.log(` Journal: ${v.journal.length} event(s) from ${JOURNAL_SIDECAR} sidecar`);
17799
+ if (v.sidecarLoaded || v.archivesLoaded.length > 0) {
17800
+ const from = [
17801
+ ...v.sidecarLoaded ? [`${JOURNAL_SIDECAR} sidecar`] : [],
17802
+ ...v.archivesLoaded.length > 0 ? [`${v.archivesLoaded.length} archive(s)`] : []
17803
+ ].join(" + ");
17804
+ console.log(` Journal: ${v.journal.length} event(s) from ${from}`);
16794
17805
  } else if (v.journal.length > 0) {
16795
17806
  console.log(` Journal: ${v.journal.length} embedded event(s)`);
16796
17807
  }
@@ -16800,10 +17811,7 @@ function validate(filePath) {
16800
17811
  v.result.warnings.forEach((w) => console.log(` ${formatFinding(w)}`));
16801
17812
  console.log("");
16802
17813
  }
16803
- const errorLines = [
16804
- ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
16805
- ...v.result.errors.map(formatFinding)
16806
- ];
17814
+ const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
16807
17815
  if (errorLines.length === 0) {
16808
17816
  console.log(" Result: VALID\n");
16809
17817
  process.exit(0);
@@ -16843,59 +17851,9 @@ ${USAGE2}`);
16843
17851
  }
16844
17852
  }
16845
17853
 
16846
- // src/lib/journal-io.ts
16847
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
16848
- import { dirname as dirname3, join as join3 } from "node:path";
16849
- var JOURNAL_SIDECAR2 = "journal.jsonl";
16850
- function journalPathFor(bundlePath) {
16851
- return join3(dirname3(bundlePath), JOURNAL_SIDECAR2);
16852
- }
16853
- function archivePathFor(journalPath, version2) {
16854
- return join3(dirname3(journalPath), "journal", `archive-${version2}.jsonl`);
16855
- }
16856
- function readJournalEvents(journalPath) {
16857
- if (!existsSync4(journalPath)) return [];
16858
- return parseJournalLines(readFileSync5(journalPath, "utf8")).events;
16859
- }
16860
- function loadJournalEvents(bundle, bundlePath) {
16861
- if (Array.isArray(bundle.journal)) return bundle.journal;
16862
- return readJournalEvents(journalPathFor(bundlePath));
16863
- }
16864
- function toLine(event) {
16865
- return JSON.stringify(event) + "\n";
16866
- }
16867
- function appendJournalEvent(journalPath, event) {
16868
- const line2 = toLine(event);
16869
- if (!existsSync4(journalPath)) {
16870
- mkdirSync2(dirname3(journalPath), { recursive: true });
16871
- writeFileSync3(journalPath, line2);
16872
- return;
16873
- }
16874
- const existing = readFileSync5(journalPath, "utf8");
16875
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
16876
- appendFileSync(journalPath, prefix + line2);
16877
- }
16878
- function compactSlice(journalPath, slice, version2) {
16879
- if (slice.length === 0) return;
16880
- const sliceIds = new Set(slice.map((ev) => ev.id));
16881
- const all = readJournalEvents(journalPath);
16882
- const surviving = all.filter((ev) => !sliceIds.has(ev.id));
16883
- const archivePath = archivePathFor(journalPath, version2);
16884
- mkdirSync2(dirname3(archivePath), { recursive: true });
16885
- const archiveLines = slice.map(toLine).join("");
16886
- if (existsSync4(archivePath)) {
16887
- const existing = readFileSync5(archivePath, "utf8");
16888
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
16889
- appendFileSync(archivePath, prefix + archiveLines);
16890
- } else {
16891
- writeFileSync3(archivePath, archiveLines);
16892
- }
16893
- writeFileSync3(journalPath, surviving.map(toLine).join(""));
16894
- }
16895
-
16896
- // src/lib/render-event.ts
16897
- function str(value) {
16898
- return typeof value === "string" ? value : void 0;
17854
+ // src/lib/render-event.ts
17855
+ function str(value) {
17856
+ return typeof value === "string" ? value : void 0;
16899
17857
  }
16900
17858
  function title(id, nodesById) {
16901
17859
  const nodeId = str(id);
@@ -16957,6 +17915,10 @@ function renderEventLine(event, nodesById) {
16957
17915
  const to = str(event.to) ?? "?";
16958
17916
  return `${title(event.node_id, nodesById)}: reference ${from ? `${from} -> ${to}` : to}`;
16959
17917
  }
17918
+ case "journal.baseline": {
17919
+ const count = Array.isArray(event.node_ids) ? event.node_ids.length : 0;
17920
+ return `Journal baseline: ${count} pre-existing node(s) recorded`;
17921
+ }
16960
17922
  default:
16961
17923
  return event.type;
16962
17924
  }
@@ -17160,7 +18122,12 @@ ${USAGE4}`);
17160
18122
  } catch (e) {
17161
18123
  fail4(`FATAL: could not build release event \u2014 ${e.message}`);
17162
18124
  }
18125
+ const baseline = ensureJournalBaseline(journalPath, bundle, ACTOR);
17163
18126
  appendJournalEvent(journalPath, event);
18127
+ if (baseline !== void 0) {
18128
+ console.log(`
18129
+ ${renderEventLine(baseline)} -> ${journalPath}`);
18130
+ }
17164
18131
  console.log(`
17165
18132
  Tagged release ${version2}${platform ? ` [${platform}]` : ""} -> ${journalPath}`);
17166
18133
  const all = [...existing, event];
@@ -17317,7 +18284,12 @@ ${USAGE5}`);
17317
18284
  fail5(`FATAL: could not build deliverable event \u2014 ${e.message}`);
17318
18285
  }
17319
18286
  const journalPath = journalPathFor(filePath);
18287
+ const baseline = ensureJournalBaseline(journalPath, bundle, ACTOR2);
17320
18288
  appendJournalEvent(journalPath, event);
18289
+ if (baseline !== void 0) {
18290
+ console.log(`
18291
+ ${renderEventLine(baseline)} -> ${journalPath}`);
18292
+ }
17321
18293
  console.log(
17322
18294
  `
17323
18295
  Recorded deliverable ${deliverableId} \u2014 ${title2} -> ${journalPath}
@@ -17504,6 +18476,13 @@ async function runSync(options = {}) {
17504
18476
  const errors = [];
17505
18477
  const nodeTitles = /* @__PURE__ */ new Map();
17506
18478
  let dirty = false;
18479
+ let baseline;
18480
+ let baselineChecked = false;
18481
+ const adoptJournal = () => {
18482
+ if (baselineChecked) return;
18483
+ baselineChecked = true;
18484
+ baseline = ensureJournalBaseline(journalPath, bundle, actor);
18485
+ };
17507
18486
  for (const node of nodes) {
17508
18487
  const nodeId = typeof node.id === "string" ? node.id : void 0;
17509
18488
  if (nodeId !== void 0 && typeof node.title === "string") nodeTitles.set(nodeId, node.title);
@@ -17556,6 +18535,7 @@ async function runSync(options = {}) {
17556
18535
  },
17557
18536
  { actor, ts: syncedAt }
17558
18537
  );
18538
+ adoptJournal();
17559
18539
  appendJournalEvent(journalPath, event);
17560
18540
  }
17561
18541
  }
@@ -17570,6 +18550,7 @@ async function runSync(options = {}) {
17570
18550
  const patch = promotionPatch(node, promotion);
17571
18551
  Object.assign(node, patch);
17572
18552
  dirty = true;
18553
+ adoptJournal();
17573
18554
  appendJournalEvent(
17574
18555
  journalPath,
17575
18556
  makeEvent(
@@ -17588,7 +18569,7 @@ async function runSync(options = {}) {
17588
18569
  if (dirty && !dryRun) {
17589
18570
  writeFileSync4(filePath, serializeBundle(bundle));
17590
18571
  }
17591
- return { ok: true, bundlePath: filePath, journalPath, dryRun, changed, unchanged, skipped, errors, nodeTitles, promoted };
18572
+ return { ok: true, bundlePath: filePath, journalPath, dryRun, changed, unchanged, skipped, errors, nodeTitles, promoted, baseline };
17592
18573
  }
17593
18574
  function changeLine(change, nodeTitles) {
17594
18575
  const event = {
@@ -17626,6 +18607,9 @@ function report(result) {
17626
18607
  for (const s of stubSkips) byProvider.set(s.provider ?? "?", (byProvider.get(s.provider ?? "?") ?? 0) + 1);
17627
18608
  for (const [provider, count] of byProvider) console.log(` - ${provider}: ${count} ref(s)`);
17628
18609
  }
18610
+ if (result.baseline !== void 0) {
18611
+ console.log(` ${renderEventLine(result.baseline)}`);
18612
+ }
17629
18613
  if (result.errors.length > 0) {
17630
18614
  console.log(` Errors: ${result.errors.length}`);
17631
18615
  result.errors.forEach((e) => console.log(` - ${e.nodeId}/${e.refId} (${e.refType}): ${e.message}`));
@@ -17669,18 +18653,288 @@ ${USAGE6}`);
17669
18653
  }
17670
18654
 
17671
18655
  // src/commands/pack.ts
18656
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
18657
+ import { dirname as dirname5, extname, resolve as resolve5 } from "node:path";
18658
+
18659
+ // src/lib/kritik-io.ts
18660
+ import { existsSync as existsSync7 } from "node:fs";
18661
+ import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
18662
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
18663
+
18664
+ // ../schema/src/cli/kritik-audit.ts
18665
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "node:fs";
18666
+ import { join as join4 } from "node:path";
18667
+
18668
+ // ../schema/src/cli/kritik-paths.ts
17672
18669
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
17673
- import { dirname as dirname4, extname, resolve as resolve3 } from "node:path";
17674
- var DEFAULT_BUNDLE_PATH5 = "docs/arkaik/bundle.json";
17675
- var USAGE7 = `arkaik pack [--no-journal] [--inline-assets] [--out <path>] [path]
18670
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
18671
+ var QUALITY_DIR = "docs/quality";
18672
+ var PROFILE_FILE = "profile.json";
18673
+ var OVERLAY_FILE = "criteria.custom.json";
18674
+ var AUDITS_DIR = "audits";
18675
+ var PACK_FILE = "library.json";
18676
+ function readJson(path6) {
18677
+ const text = readFileSync6(path6, "utf8");
18678
+ try {
18679
+ return JSON.parse(text);
18680
+ } catch (e) {
18681
+ throw new Error(`${path6}: not valid JSON \u2014 ${e.message}`);
18682
+ }
18683
+ }
18684
+ function writeJson(path6, value) {
18685
+ mkdirSync3(dirname3(path6), { recursive: true });
18686
+ writeFileSync5(path6, JSON.stringify(value, null, 2) + "\n");
18687
+ }
18688
+ var profilePath = (root) => join3(root, QUALITY_DIR, PROFILE_FILE);
18689
+ var overlayPath = (root) => join3(root, QUALITY_DIR, OVERLAY_FILE);
18690
+ var auditsDir = (root) => join3(root, QUALITY_DIR, AUDITS_DIR);
18691
+ var auditDir = (root, auditId) => join3(auditsDir(root), auditId);
18692
+ function loadProfile(root) {
18693
+ const path6 = profilePath(root);
18694
+ return existsSync5(path6) ? readJson(path6) : null;
18695
+ }
18696
+ function loadOverlay(root) {
18697
+ const path6 = overlayPath(root);
18698
+ return existsSync5(path6) ? readJson(path6) : null;
18699
+ }
18700
+
18701
+ // ../schema/src/cli/kritik-audit.ts
18702
+ var SCORES_FILE = "scores.json";
18703
+ var FINDINGS_FILE = "findings.json";
18704
+ var MATRIX_FILE = "matrix.json";
18705
+ var scoresPath = (root, auditId) => join4(auditDir(root, auditId), SCORES_FILE);
18706
+ var findingsPath = (root, auditId) => join4(auditDir(root, auditId), FINDINGS_FILE);
18707
+ var matrixPath = (root, auditId) => join4(auditDir(root, auditId), MATRIX_FILE);
18708
+ function listAuditIds(root) {
18709
+ const dir = auditsDir(root);
18710
+ if (!existsSync6(dir)) return [];
18711
+ return readdirSync2(dir).filter((name) => statSync(join4(dir, name)).isDirectory()).sort();
18712
+ }
18713
+ function newestAuditId(root) {
18714
+ const dir = auditsDir(root);
18715
+ if (!existsSync6(dir)) throw new Error(`no audits directory at ${dir}`);
18716
+ const ids = listAuditIds(root);
18717
+ if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
18718
+ return ids[ids.length - 1];
18719
+ }
18720
+ function loadScores(root, auditId) {
18721
+ const path6 = scoresPath(root, auditId);
18722
+ if (!existsSync6(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
18723
+ const file2 = readJson(path6);
18724
+ return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
18725
+ }
18726
+ function loadScoresOrEmpty(root, auditId) {
18727
+ return existsSync6(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
18728
+ }
18729
+ function loadFindings(root, auditId) {
18730
+ const path6 = findingsPath(root, auditId);
18731
+ if (!existsSync6(path6)) return { audit_id: auditId, findings: [] };
18732
+ const file2 = readJson(path6);
18733
+ return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
18734
+ }
18735
+ function saveScores(root, auditId, file2) {
18736
+ writeJson(scoresPath(root, auditId), file2);
18737
+ }
18738
+ function saveFindings(root, auditId, file2) {
18739
+ writeJson(findingsPath(root, auditId), file2);
18740
+ }
18741
+ function requireProfile(root) {
18742
+ const profile = loadProfile(root);
18743
+ if (!profile) {
18744
+ throw new Error(
18745
+ `no profile at ${join4(root, QUALITY_DIR, "profile.json")} \u2014 pick this project's surfaces first (\`arkaik kritik profile\`, or the plugin's init-profile.js).`
18746
+ );
18747
+ }
18748
+ return profile;
18749
+ }
18750
+ function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
18751
+ const findings = loadFindings(root, auditId);
18752
+ return {
18753
+ framework_version: scores.framework_version ?? library.version,
18754
+ profile: requireProfile(root),
18755
+ assessments: scores.assessments,
18756
+ findings: findings.findings
18757
+ };
18758
+ }
18759
+ function stripDerived(finding) {
18760
+ const { severity: _severity, priority: _priority, ...rest } = finding;
18761
+ void _severity;
18762
+ void _priority;
18763
+ return rest;
18764
+ }
18765
+ function requireAudit(root, auditId) {
18766
+ if (!listAuditIds(root).includes(auditId)) {
18767
+ throw new Error(`no audit "${auditId}" under ${auditsDir(root)}`);
18768
+ }
18769
+ }
18770
+ function loadCurrentQualitySection(root, library) {
18771
+ const auditIds = listAuditIds(root);
18772
+ if (auditIds.length === 0) return void 0;
18773
+ const profile = loadProfile(root);
18774
+ if (!profile) return void 0;
18775
+ const cells = /* @__PURE__ */ new Map();
18776
+ const findings = [];
18777
+ let frameworkVersion;
18778
+ for (const id of auditIds) {
18779
+ const scores = loadScoresOrEmpty(root, id);
18780
+ if (typeof scores.framework_version === "string") frameworkVersion = scores.framework_version;
18781
+ for (const assessment of scores.assessments) {
18782
+ cells.set(`${assessment.criterion_id}\0${assessment.surface}`, assessment);
18783
+ }
18784
+ for (const finding of loadFindings(root, id).findings) findings.push(stripDerived(finding));
18785
+ }
18786
+ return {
18787
+ framework_version: frameworkVersion ?? library.version,
18788
+ library,
18789
+ profile,
18790
+ assessments: [...cells.values()],
18791
+ findings
18792
+ };
18793
+ }
18794
+ function loadAuditQualitySection(root, auditId, library) {
18795
+ requireAudit(root, auditId);
18796
+ const section = loadQualitySection(root, auditId, library, loadScoresOrEmpty(root, auditId));
18797
+ return { ...section, library, findings: section.findings.map(stripDerived) };
18798
+ }
18799
+ function computeAuditMatrix(root, auditId, library) {
18800
+ const scores = loadScores(root, auditId);
18801
+ const section = loadQualitySection(root, auditId, library, scores);
18802
+ const matrix = deriveQualityMatrix({ quality: section }, library);
18803
+ const file2 = {
18804
+ audit_id: auditId,
18805
+ commit: scores.commit,
18806
+ framework_version: section.framework_version,
18807
+ matrix: matrix.matrix,
18808
+ overall: matrix.overall,
18809
+ finding_counts: matrix.finding_counts
18810
+ };
18811
+ writeJson(matrixPath(root, auditId), file2);
18812
+ return { section, matrix, file: file2 };
18813
+ }
18814
+ function renderMatrixMarkdown(matrix, domainNames) {
18815
+ const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
18816
+ const lines = [];
18817
+ lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
18818
+ lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
18819
+ for (const domain2 of matrix.domains) {
18820
+ const label = domainNames.get(domain2) ?? domain2;
18821
+ lines.push(
18822
+ `| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
18823
+ );
18824
+ }
18825
+ lines.push(
18826
+ `| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
18827
+ const score = matrix.overall[s];
18828
+ return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
18829
+ }).join(" | ")} |`
18830
+ );
18831
+ return lines.join("\n");
18832
+ }
18833
+ function locateFinding(root, id) {
18834
+ for (const auditId of [...listAuditIds(root)].reverse()) {
18835
+ const file2 = loadFindings(root, auditId);
18836
+ const finding = file2.findings.find((candidate) => candidate.id === id);
18837
+ if (finding) return { auditId, file: file2, finding };
18838
+ }
18839
+ return void 0;
18840
+ }
18841
+
18842
+ // src/lib/kritik-io.ts
18843
+ var KRITIK_ACTOR = "arkaik-cli";
18844
+ var DEFAULT_BUNDLE_PATH5 = join5("docs", "arkaik", "bundle.json");
18845
+ var VENDORED_PACK = join5(QUALITY_DIR, PACK_FILE);
18846
+ var BUNDLED_PACK = join5(dirname4(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
18847
+ function resolvePack(root) {
18848
+ const vendored = join5(root, VENDORED_PACK);
18849
+ if (existsSync7(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
18850
+ if (!existsSync7(BUNDLED_PACK)) {
18851
+ throw new Error(
18852
+ `no criteria pack found. Looked in:
18853
+ ${vendored}
18854
+ ${BUNDLED_PACK}
18855
+ The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
18856
+ );
18857
+ }
18858
+ return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
18859
+ }
18860
+ function loadKritikLibrary(root) {
18861
+ const pack = resolvePack(root);
18862
+ return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
18863
+ }
18864
+ function resolveJournal(root, bundlePath) {
18865
+ const resolved = bundlePath ?? join5(root, DEFAULT_BUNDLE_PATH5);
18866
+ return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync7(resolved) };
18867
+ }
18868
+ function appendQualityEvents(root, inputs, options = {}) {
18869
+ if (inputs.length === 0) return { events: [] };
18870
+ const actor = options.actor ?? KRITIK_ACTOR;
18871
+ const journal = resolveJournal(root, options.bundlePath);
18872
+ if (!journal.present) return { events: [] };
18873
+ const bundle = readBundle(journal.bundlePath);
18874
+ const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
18875
+ const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
18876
+ for (const event of events) appendJournalEvent(journal.journalPath, event);
18877
+ return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
18878
+ }
18879
+ function isQualitySection(value) {
18880
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18881
+ }
18882
+ function foldQualitySection(bundle, root, auditId) {
18883
+ const notFolded = (reason) => {
18884
+ const carriedSection = isQualitySection(bundle.quality);
18885
+ return {
18886
+ folded: false,
18887
+ carriedSection,
18888
+ notice: carriedSection ? `Quality: ${reason}
18889
+ Kept the quality section this bundle already carried \u2014 nothing replaced it.` : `Quality: ${reason}`
18890
+ };
18891
+ };
18892
+ if (auditId !== void 0) requireAudit(root, auditId);
18893
+ const auditIds = listAuditIds(root);
18894
+ if (auditIds.length === 0) {
18895
+ return notFolded(`none to fold \u2014 no audits under ${auditsDir(root)} (run \`arkaik kritik score\` to open one)`);
18896
+ }
18897
+ if (!loadProfile(root)) {
18898
+ return notFolded(`skipped, no profile \u2014 nothing at ${profilePath(root)} (run \`arkaik kritik profile\`)`);
18899
+ }
18900
+ let library;
18901
+ try {
18902
+ ({ library } = loadKritikLibrary(root));
18903
+ } catch (e) {
18904
+ return notFolded(`skipped, no pack \u2014 ${e.message}`);
18905
+ }
18906
+ const section = auditId === void 0 ? loadCurrentQualitySection(root, library) : loadAuditQualitySection(root, auditId, library);
18907
+ if (section === void 0) {
18908
+ return notFolded(`nothing to fold \u2014 no audits under ${auditsDir(root)}, or no profile at ${profilePath(root)}`);
18909
+ }
18910
+ bundle.quality = section;
18911
+ const count = auditId === void 0 ? auditIds.length : 1;
18912
+ return {
18913
+ folded: true,
18914
+ carriedSection: false,
18915
+ notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
18916
+ };
18917
+ }
18918
+ function resolveQualityRoot(cwd, filePath, root) {
18919
+ if (root !== void 0) return resolve4(cwd, root);
18920
+ const dir = dirname4(filePath);
18921
+ if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
18922
+ return cwd;
18923
+ }
18924
+
18925
+ // src/commands/pack.ts
18926
+ var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
18927
+ var USAGE7 = `arkaik pack [--no-journal] [--no-quality] [--inline-assets] [--audit <id>]
18928
+ [--root <dir>] [--out <path>] [path]
17676
18929
 
17677
18930
  Produce a single self-contained interchange bundle: fold in the sidecar
17678
- journal (or keep an existing embedded one) and, with --inline-assets, inline
17679
- local screenshot files as data: URIs. Written canonically via serializeBundle.
17680
- Unknown top-level keys and unknown fields always round-trip.
18931
+ journal (or keep an existing embedded one), fold docs/quality/ into the
18932
+ quality section, and, with --inline-assets, inline local screenshot files as
18933
+ data: URIs. Written canonically via serializeBundle. Unknown top-level keys
18934
+ and unknown fields always round-trip.
17681
18935
 
17682
18936
  Arguments:
17683
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH5}).
18937
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
17684
18938
 
17685
18939
  Options:
17686
18940
  --no-journal Omit the embedded journal[] (Publik-safe posture \u2014 history
@@ -17689,12 +18943,33 @@ Options:
17689
18943
  interchange) \u2014 embedded wins over the sidecar when the
17690
18944
  bundle already carries one, otherwise the sidecar is used
17691
18945
  (same precedence "arkaik validate" folds by).
18946
+ --no-quality DELETE the quality section rather than folding one in \u2014
18947
+ not merely "skip the fold", because the source bundle may
18948
+ already carry a section of its own and that one goes too.
18949
+ For any bundle that must not travel with open findings:
18950
+ a finding names an unfixed vulnerability and the file to
18951
+ find it in. ("arkaik push" packs this way by default;
18952
+ its --include-quality opts back in.)
18953
+ Default: docs/quality/ IS folded in.
17692
18954
  --inline-assets Convert relative-path metadata.platformScreenshots values
17693
18955
  into data: URIs by reading the file from disk (resolved
17694
18956
  against the bundle's directory). Absolute https:// URLs
17695
18957
  and existing data: URIs are left as-is. v1 scope: local
17696
18958
  files only \u2014 uploading a remote/hosted copy is not
17697
18959
  implemented.
18960
+ --audit <id> Pin ONE audit's snapshot instead of the default merge.
18961
+ They answer different questions: the merge (every audit,
18962
+ latest score per criterion x surface) answers "where does
18963
+ the product stand", while a pinned audit answers "how did
18964
+ that audit go" \u2014 the question its own matrix.json
18965
+ answers. An id that is not on disk is an error, not an
18966
+ empty section.
18967
+ --root <dir> Where docs/quality/ lives. Default: NOT the current
18968
+ directory \u2014 it is derived from the bundle's own path, so
18969
+ packing <repo>/docs/arkaik/bundle.json folds <repo>'s
18970
+ audits whatever directory you run from. Only a bundle
18971
+ kept outside that conventional layout falls back to the
18972
+ cwd, and that is the case this flag is for.
17698
18973
  --out <path> Write the packed bundle here instead of stdout.
17699
18974
  -h, --help Show this help.`;
17700
18975
  function fail7(message) {
@@ -17732,7 +19007,7 @@ function fatalResult2(bundlePath, message) {
17732
19007
  }
17733
19008
  function runPack(options = {}) {
17734
19009
  const cwd = options.cwd ?? process.cwd();
17735
- const filePath = resolve3(cwd, options.path ?? DEFAULT_BUNDLE_PATH5);
19010
+ const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
17736
19011
  const noJournal = options.noJournal ?? false;
17737
19012
  const inlineAssets = options.inlineAssets ?? false;
17738
19013
  let bundle;
@@ -17753,10 +19028,24 @@ function runPack(options = {}) {
17753
19028
  journalEventCount = events.length;
17754
19029
  }
17755
19030
  }
19031
+ let qualityFolded;
19032
+ let qualityNotice;
19033
+ if (options.noQuality ?? false) {
19034
+ delete bundle.quality;
19035
+ } else {
19036
+ const root = resolveQualityRoot(cwd, filePath, options.root);
19037
+ try {
19038
+ const fold = foldQualitySection(bundle, root, options.audit);
19039
+ qualityFolded = fold.folded;
19040
+ qualityNotice = fold.notice;
19041
+ } catch (e) {
19042
+ return fatalResult2(filePath, e.message);
19043
+ }
19044
+ }
17756
19045
  const inlinedAssets = [];
17757
19046
  const assetWarnings = [];
17758
19047
  if (inlineAssets) {
17759
- const bundleDir = dirname4(filePath);
19048
+ const bundleDir = dirname5(filePath);
17760
19049
  const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
17761
19050
  for (const node of nodes) {
17762
19051
  const nodeId = typeof node.id === "string" ? node.id : "?";
@@ -17767,12 +19056,12 @@ function runPack(options = {}) {
17767
19056
  const map2 = screenshots;
17768
19057
  for (const [platform, value] of Object.entries(map2)) {
17769
19058
  if (typeof value !== "string" || !isRelativeAssetPath(value)) continue;
17770
- const assetPath = resolve3(bundleDir, value);
17771
- if (!existsSync5(assetPath)) {
19059
+ const assetPath = resolve5(bundleDir, value);
19060
+ if (!existsSync8(assetPath)) {
17772
19061
  assetWarnings.push(`${nodeId}/${platform}: asset not found at ${assetPath} \u2014 left as-is`);
17773
19062
  continue;
17774
19063
  }
17775
- const bytes = readFileSync6(assetPath);
19064
+ const bytes = readFileSync7(assetPath);
17776
19065
  const mime = mimeForExtension(extname(assetPath));
17777
19066
  map2[platform] = `data:${mime};base64,${bytes.toString("base64")}`;
17778
19067
  inlinedAssets.push({ nodeId, platform, path: value });
@@ -17782,15 +19071,18 @@ function runPack(options = {}) {
17782
19071
  const output = serializeBundle(bundle);
17783
19072
  let outPath;
17784
19073
  if (options.out !== void 0) {
17785
- outPath = resolve3(cwd, options.out);
17786
- mkdirSync3(dirname4(outPath), { recursive: true });
17787
- writeFileSync5(outPath, output);
19074
+ outPath = resolve5(cwd, options.out);
19075
+ mkdirSync4(dirname5(outPath), { recursive: true });
19076
+ writeFileSync6(outPath, output);
17788
19077
  }
17789
- return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, output };
19078
+ return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, qualityFolded, qualityNotice, output };
17790
19079
  }
17791
19080
  function runPackCli(args) {
17792
19081
  let noJournal = false;
19082
+ let noQuality = false;
17793
19083
  let inlineAssets = false;
19084
+ let audit;
19085
+ let root;
17794
19086
  let out;
17795
19087
  const positionals = [];
17796
19088
  for (let i = 0; i < args.length; i++) {
@@ -17800,8 +19092,22 @@ function runPackCli(args) {
17800
19092
  process.exit(0);
17801
19093
  } else if (arg === "--no-journal") {
17802
19094
  noJournal = true;
19095
+ } else if (arg === "--no-quality") {
19096
+ noQuality = true;
17803
19097
  } else if (arg === "--inline-assets") {
17804
19098
  inlineAssets = true;
19099
+ } else if (arg === "--audit") {
19100
+ const value = args[++i];
19101
+ if (value === void 0) fail7(`Missing value for --audit
19102
+
19103
+ ${USAGE7}`);
19104
+ audit = value;
19105
+ } else if (arg === "--root") {
19106
+ const value = args[++i];
19107
+ if (value === void 0) fail7(`Missing value for --root
19108
+
19109
+ ${USAGE7}`);
19110
+ root = value;
17805
19111
  } else if (arg === "--out") {
17806
19112
  const value = args[++i];
17807
19113
  if (value === void 0) fail7(`Missing value for --out
@@ -17816,8 +19122,13 @@ ${USAGE7}`);
17816
19122
  positionals.push(arg);
17817
19123
  }
17818
19124
  }
17819
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH5;
17820
- const result = runPack({ path: filePath, out, noJournal, inlineAssets });
19125
+ if (noQuality && audit !== void 0) {
19126
+ fail7(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
19127
+
19128
+ ${USAGE7}`);
19129
+ }
19130
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
19131
+ const result = runPack({ path: filePath, out, noJournal, inlineAssets, noQuality, audit, root });
17821
19132
  if (!result.ok) fail7(`FATAL: ${result.fatal}`);
17822
19133
  if (result.journalIncluded) {
17823
19134
  console.error(`Journal: embedded ${result.journalEventCount} event(s)`);
@@ -17826,6 +19137,9 @@ ${USAGE7}`);
17826
19137
  } else {
17827
19138
  console.error("Journal: none to embed (no embedded journal, no sidecar)");
17828
19139
  }
19140
+ if (result.qualityNotice !== void 0) {
19141
+ console.error(result.qualityNotice);
19142
+ }
17829
19143
  for (const asset of result.inlinedAssets) {
17830
19144
  console.error(`Inlined asset: ${asset.nodeId}/${asset.platform} (${asset.path})`);
17831
19145
  }
@@ -17842,10 +19156,10 @@ ${USAGE7}`);
17842
19156
 
17843
19157
  // src/commands/open.ts
17844
19158
  import { spawn } from "node:child_process";
17845
- import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
19159
+ import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
17846
19160
  import { tmpdir } from "node:os";
17847
- import { join as join4, resolve as resolve4 } from "node:path";
17848
- var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
19161
+ import { join as join6, resolve as resolve6 } from "node:path";
19162
+ var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
17849
19163
  var OPEN_URL = "https://arkaik.app/projects";
17850
19164
  var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
17851
19165
 
@@ -17856,7 +19170,7 @@ ${OPEN_URL} (the project list's "Import JSON" picker). On an invalid bundle,
17856
19170
  findings are printed and nothing is packed, written, or opened.
17857
19171
 
17858
19172
  Arguments:
17859
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
19173
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
17860
19174
 
17861
19175
  Options:
17862
19176
  --out <path> Write the packed bundle here instead of a temp file.
@@ -17878,7 +19192,7 @@ function fatalResult3(bundlePath, message) {
17878
19192
  }
17879
19193
  async function runOpen(options = {}) {
17880
19194
  const cwd = options.cwd ?? process.cwd();
17881
- const filePath = resolve4(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
19195
+ const filePath = resolve6(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
17882
19196
  const noOpen = options.noOpen ?? false;
17883
19197
  let v;
17884
19198
  try {
@@ -17887,10 +19201,7 @@ async function runOpen(options = {}) {
17887
19201
  return fatalResult3(filePath, e.message);
17888
19202
  }
17889
19203
  const warningLines = v.result.warnings.map(formatFinding);
17890
- const errorLines = [
17891
- ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
17892
- ...v.result.errors.map(formatFinding)
17893
- ];
19204
+ const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
17894
19205
  if (!v.valid) {
17895
19206
  return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, opened: false };
17896
19207
  }
@@ -17900,9 +19211,9 @@ async function runOpen(options = {}) {
17900
19211
  }
17901
19212
  let outPath = packed.outPath;
17902
19213
  if (outPath === void 0) {
17903
- const dir = mkdtempSync(join4(tmpdir(), "arkaik-open-"));
17904
- outPath = join4(dir, "bundle.json");
17905
- writeFileSync6(outPath, packed.output);
19214
+ const dir = mkdtempSync(join6(tmpdir(), "arkaik-open-"));
19215
+ outPath = join6(dir, "bundle.json");
19216
+ writeFileSync7(outPath, packed.output);
17906
19217
  }
17907
19218
  let opened = false;
17908
19219
  if (!noOpen) {
@@ -17910,7 +19221,17 @@ async function runOpen(options = {}) {
17910
19221
  await opener(OPEN_URL);
17911
19222
  opened = true;
17912
19223
  }
17913
- return { ok: true, bundlePath: filePath, valid: true, errorLines, warningLines, outPath, url: OPEN_URL, opened };
19224
+ return {
19225
+ ok: true,
19226
+ bundlePath: filePath,
19227
+ valid: true,
19228
+ errorLines,
19229
+ warningLines,
19230
+ outPath,
19231
+ url: OPEN_URL,
19232
+ opened,
19233
+ qualityNotice: packed.qualityNotice
19234
+ };
17914
19235
  }
17915
19236
  function runOpenCli(args) {
17916
19237
  let out;
@@ -17937,7 +19258,7 @@ ${USAGE8}`);
17937
19258
  positionals.push(arg);
17938
19259
  }
17939
19260
  }
17940
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
19261
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
17941
19262
  runOpen({ path: filePath, out, noOpen }).then((result) => {
17942
19263
  if (!result.ok) fail8(`FATAL: ${result.fatal}`);
17943
19264
  if (result.warningLines.length > 0) {
@@ -17950,6 +19271,9 @@ ${USAGE8}`);
17950
19271
  console.error("\nInvalid bundle \u2014 not packed, not opened.");
17951
19272
  process.exit(1);
17952
19273
  }
19274
+ if (result.qualityNotice !== void 0) {
19275
+ console.error(result.qualityNotice);
19276
+ }
17953
19277
  console.log(`Packed -> ${result.outPath}`);
17954
19278
  if (result.opened) {
17955
19279
  console.log(`Opened ${result.url}`);
@@ -17961,10 +19285,10 @@ ${USAGE8}`);
17961
19285
  }
17962
19286
 
17963
19287
  // src/commands/push.ts
17964
- import { resolve as resolve5 } from "node:path";
17965
- var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
19288
+ import { resolve as resolve7 } from "node:path";
19289
+ var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
17966
19290
  var DEFAULT_API_BASE = "https://arkaik.app";
17967
- var USAGE9 = `arkaik push [--include-journal] [--api <base-url>] [path]
19291
+ var USAGE9 = `arkaik push [--include-journal] [--include-quality] [--api <base-url>] [path]
17968
19292
  arkaik push --delete <id> --key <owner_key> [--api <base-url>]
17969
19293
 
17970
19294
  Publish a project bundle to Publik (anonymous, account-less snapshot
@@ -17978,18 +19302,29 @@ Publik-safe posture (docs/spec/journal.md). --include-journal embeds it
17978
19302
  (like a bare "arkaik pack") and forwards ?include_journal=true so the server
17979
19303
  knows to keep it.
17980
19304
 
19305
+ A Kritik "quality" section is stripped by default too, and separately:
19306
+ publishing your history and publishing your open findings are two decisions,
19307
+ not one, and both default to no. --include-quality opts in.
19308
+
17981
19309
  Snapshots are immutable: there is no update verb. Pushing again always mints
17982
19310
  a new id. The owner key printed on success is shown exactly once and cannot
17983
19311
  be recovered \u2014 save it if you may need to delete the snapshot later.
17984
19312
 
17985
19313
  Arguments:
17986
- path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
19314
+ path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH8}).
17987
19315
  Ignored with --delete.
17988
19316
 
17989
19317
  Options:
17990
19318
  --include-journal Embed the journal in the pushed bundle and forward
17991
19319
  ?include_journal=true. Default: stripped, omitted
17992
19320
  entirely from the request body.
19321
+ --include-quality Embed the Kritik quality section and forward
19322
+ ?include_quality=true. Opt-in rather than opt-out
19323
+ because an open finding is an unfixed vulnerability
19324
+ plus the path to find it in (docs/rfcs/kritik.md
19325
+ \xA7 8.3) \u2014 publishing that is a decision worth typing.
19326
+ Default: deleted before packing, so it is never in the
19327
+ request body at all.
17993
19328
  --api <base-url> Publik API base URL (default: ${DEFAULT_API_BASE}).
17994
19329
  Point at a self-hosted deployment.
17995
19330
  --delete <id> Delete a snapshot by id instead of pushing. Requires
@@ -18014,8 +19349,9 @@ function fatalResult4(bundlePath, message) {
18014
19349
  }
18015
19350
  async function runPush(options = {}) {
18016
19351
  const cwd = options.cwd ?? process.cwd();
18017
- const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
19352
+ const filePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
18018
19353
  const includeJournal = options.includeJournal ?? false;
19354
+ const includeQuality = options.includeQuality ?? false;
18019
19355
  const apiBase = options.apiBase ?? DEFAULT_API_BASE;
18020
19356
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
18021
19357
  let v;
@@ -18025,18 +19361,19 @@ async function runPush(options = {}) {
18025
19361
  return fatalResult4(filePath, e.message);
18026
19362
  }
18027
19363
  const warningLines = v.result.warnings.map(formatFinding);
18028
- const errorLines = [
18029
- ...v.sidecarFindings.map((f) => `ERROR [${f.rule}] line ${f.line}: ${f.message}`),
18030
- ...v.result.errors.map(formatFinding)
18031
- ];
19364
+ const errorLines = [...journalLineErrorLines(v), ...v.result.errors.map(formatFinding)];
18032
19365
  if (!v.valid) {
18033
19366
  return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, requestSent: false };
18034
19367
  }
18035
- const packed = runPack({ path: filePath, noJournal: !includeJournal, cwd });
19368
+ const packed = runPack({ path: filePath, noJournal: !includeJournal, noQuality: !includeQuality, cwd });
18036
19369
  if (!packed.ok) {
18037
19370
  return fatalResult4(filePath, packed.fatal ?? "pack failed");
18038
19371
  }
18039
- const url2 = `${apiBase}/api/publik${includeJournal ? "?include_journal=true" : ""}`;
19372
+ const qualityNotice = packed.qualityNotice ?? "Quality: stripped, not sent (pass --include-quality to publish it)";
19373
+ const params = [];
19374
+ if (includeJournal) params.push("include_journal=true");
19375
+ if (includeQuality) params.push("include_quality=true");
19376
+ const url2 = `${apiBase}/api/publik${params.length > 0 ? `?${params.join("&")}` : ""}`;
18040
19377
  let res;
18041
19378
  try {
18042
19379
  res = await httpClient(url2, {
@@ -18066,6 +19403,7 @@ async function runPush(options = {}) {
18066
19403
  warningLines,
18067
19404
  requestSent: true,
18068
19405
  status,
19406
+ qualityNotice,
18069
19407
  id: body.id,
18070
19408
  url: body.url,
18071
19409
  ownerKey: body.owner_key
@@ -18084,6 +19422,7 @@ async function runPush(options = {}) {
18084
19422
  warningLines,
18085
19423
  requestSent: true,
18086
19424
  status,
19425
+ qualityNotice,
18087
19426
  serverFindings: errBody.findings,
18088
19427
  retryAfter: status === 429 ? res.headers.get("retry-after") : void 0,
18089
19428
  errorMessage: errBody.message ?? `Request failed with status ${status}`
@@ -18094,6 +19433,9 @@ function reportPush(result) {
18094
19433
  console.error(`Warnings: ${result.warningLines.length}`);
18095
19434
  result.warningLines.forEach((w) => console.error(` ${w}`));
18096
19435
  }
19436
+ if (result.qualityNotice !== void 0) {
19437
+ console.error(result.qualityNotice);
19438
+ }
18097
19439
  if (!result.valid) {
18098
19440
  console.error(`Errors: ${result.errorLines.length}`);
18099
19441
  result.errorLines.forEach((e) => console.error(` ${e}`));
@@ -18175,6 +19517,7 @@ function reportDelete(id, result) {
18175
19517
  }
18176
19518
  function runPushCli(args) {
18177
19519
  let includeJournal = false;
19520
+ let includeQuality = false;
18178
19521
  let apiBase;
18179
19522
  let deleteId;
18180
19523
  let key;
@@ -18186,6 +19529,8 @@ function runPushCli(args) {
18186
19529
  process.exit(0);
18187
19530
  } else if (arg === "--include-journal") {
18188
19531
  includeJournal = true;
19532
+ } else if (arg === "--include-quality") {
19533
+ includeQuality = true;
18189
19534
  } else if (arg === "--api") {
18190
19535
  const value = args[++i];
18191
19536
  if (value === void 0) fail9(`Missing value for --api
@@ -18227,16 +19572,16 @@ ${USAGE9}`);
18227
19572
  if (key !== void 0) fail9(`--key is only valid with --delete
18228
19573
 
18229
19574
  ${USAGE9}`);
18230
- const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
18231
- runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
19575
+ const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH8;
19576
+ runPush({ path: filePath, includeJournal, includeQuality, apiBase }).then((result) => {
18232
19577
  if (!result.ok) fail9(`FATAL: ${result.fatal}`);
18233
19578
  reportPush(result);
18234
19579
  }).catch((e) => fail9(`FATAL: ${e.message}`));
18235
19580
  }
18236
19581
 
18237
19582
  // src/commands/link.ts
18238
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
18239
- import { dirname as dirname5, join as join5, resolve as resolve6 } from "node:path";
19583
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
19584
+ import { dirname as dirname6, join as join7, resolve as resolve8 } from "node:path";
18240
19585
  var LINK_FILE = "docs/arkaik/arkaik.json";
18241
19586
  var DEFAULT_BASE_URL = "https://arkaik.app";
18242
19587
  var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
@@ -18307,15 +19652,15 @@ async function runLink(argv, options = {}) {
18307
19652
  return { ok: false };
18308
19653
  }
18309
19654
  const { bundle } = await res.json();
18310
- const target = resolve6(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
18311
- const linkPath = join5(target, LINK_FILE);
18312
- mkdirSync4(dirname5(linkPath), { recursive: true });
19655
+ const target = resolve8(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
19656
+ const linkPath = join7(target, LINK_FILE);
19657
+ mkdirSync5(dirname6(linkPath), { recursive: true });
18313
19658
  let existing = {};
18314
19659
  try {
18315
- existing = JSON.parse(readFileSync7(linkPath, "utf8"));
19660
+ existing = JSON.parse(readFileSync8(linkPath, "utf8"));
18316
19661
  } catch {
18317
19662
  }
18318
- writeFileSync7(
19663
+ writeFileSync8(
18319
19664
  linkPath,
18320
19665
  `${JSON.stringify({ ...existing, project_id: projectId, remote: baseUrl }, null, 2)}
18321
19666
  `
@@ -18342,14 +19687,15 @@ function runLinkCli(argv) {
18342
19687
  }
18343
19688
 
18344
19689
  // src/commands/restore.ts
18345
- import { existsSync as existsSync6, linkSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync8 } from "node:fs";
18346
- import { join as join6, resolve as resolve7 } from "node:path";
19690
+ import { existsSync as existsSync9, linkSync, mkdirSync as mkdirSync6, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
19691
+ import { join as join8, resolve as resolve9 } from "node:path";
18347
19692
  var LINK_FILE2 = "docs/arkaik/arkaik.json";
18348
- var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
19693
+ var DEFAULT_BUNDLE_PATH9 = "docs/arkaik/bundle.json";
18349
19694
  var DEFAULT_API_BASE2 = "https://arkaik.app";
18350
19695
  var USAGE11 = `arkaik restore [options] [path]
18351
19696
 
18352
- Replace the linked hosted project's bundle AND journal with a local bundle \u2014
19697
+ Replace the linked hosted project's bundle, journal AND quality section with
19698
+ a local bundle \u2014
18353
19699
  the landing step for a bootstrapped map. Before sending anything, this
18354
19700
  exports the CURRENT hosted state (snapshot + journal) to
18355
19701
  docs/arkaik/.backups/<timestamp>-bundle.json (next to the link file \u2014 not
@@ -18359,9 +19705,10 @@ only way back if the restore turns out to be wrong.
18359
19705
 
18360
19706
  Arguments:
18361
19707
  path Path to the local bundle JSON file
18362
- (default: ${DEFAULT_BUNDLE_PATH8}). Its journal.jsonl
19708
+ (default: ${DEFAULT_BUNDLE_PATH9}). Its journal.jsonl
18363
19709
  sidecar (or an embedded journal, which wins) is folded
18364
- in automatically.
19710
+ in automatically, as is the docs/quality/ tree of the
19711
+ repo that bundle belongs to.
18365
19712
 
18366
19713
  Options:
18367
19714
  --dry-run Ask the server what this restore WOULD do and print
@@ -18374,6 +19721,32 @@ Options:
18374
19721
  means a missing/gitignored journal.jsonl or a bundle
18375
19722
  from the wrong directory, not an intended history
18376
19723
  rewrite.
19724
+ --allow-deletions Proceed even though the local bundle drops nodes or
19725
+ edges the hosted project currently has. Without this
19726
+ flag, that refuses outright and names the ids \u2014 it
19727
+ usually means the hosted project moved ahead of your
19728
+ local copy (edited in the app), not an intended
19729
+ deletion. Undoing a restore from a backup is the
19730
+ common case where it IS intended.
19731
+ --no-quality Do not send a quality section: skip the docs/quality/
19732
+ fold AND drop any section the local bundle already
19733
+ carries. Does NOT by itself permit erasing the hosted
19734
+ one \u2014 that needs --allow-quality-loss too, because
19735
+ "don't send mine" and "destroy theirs" are different
19736
+ decisions and only one of them is irreversible.
19737
+ --allow-quality-loss Proceed even though the restore would erase a quality
19738
+ section the hosted project currently has. Without this
19739
+ flag, that refuses outright \u2014 it usually means
19740
+ docs/quality/ was looked for in the wrong place, which
19741
+ --root fixes, rather than an intended wipe.
19742
+ --audit <id> Fold ONE audit's snapshot rather than merging every
19743
+ audit into current state. An id that is not on disk is
19744
+ an error, not an empty section.
19745
+ --root <dir> Where docs/quality/ lives. Default: derived from the
19746
+ bundle's own path, so restoring <repo>/docs/arkaik/
19747
+ bundle.json folds <repo>'s audits whatever directory
19748
+ you run from; only a bundle kept outside that layout
19749
+ falls back to the cwd.
18377
19750
  --api <base-url> Override the remote from docs/arkaik/arkaik.json
18378
19751
  (also overridable with $ARKAIK_URL).
18379
19752
  -h, --help Show this help.
@@ -18386,8 +19759,8 @@ function fail10(message) {
18386
19759
  console.error(message);
18387
19760
  process.exit(1);
18388
19761
  }
18389
- function fatalResult5(dryRun, message) {
18390
- return { ok: false, fatal: message, dryRun, requestSent: false };
19762
+ function fatalResult5(dryRun, message, quality = {}) {
19763
+ return { ok: false, fatal: message, dryRun, requestSent: false, ...quality };
18391
19764
  }
18392
19765
  async function safeJson(res) {
18393
19766
  try {
@@ -18413,7 +19786,9 @@ async function interpretPutResponse(res, ctx) {
18413
19786
  bundlePath: ctx.bundlePath,
18414
19787
  backupPath: ctx.backupPath,
18415
19788
  requestSent: true,
18416
- status: res.status
19789
+ status: res.status,
19790
+ qualityFolded: ctx.qualityFolded,
19791
+ qualityNotice: ctx.qualityNotice
18417
19792
  };
18418
19793
  const backupNote = backupNoteFor(ctx.backupPath);
18419
19794
  if (res.status === 200) {
@@ -18477,26 +19852,91 @@ async function interpretPutResponse(res, ctx) {
18477
19852
  }
18478
19853
  function writeBackupFile(filePath, content) {
18479
19854
  const tmpPath = `${filePath}.tmp-${process.pid}`;
18480
- writeFileSync8(tmpPath, content);
19855
+ writeFileSync9(tmpPath, content);
18481
19856
  try {
18482
19857
  linkSync(tmpPath, filePath);
18483
19858
  } finally {
18484
19859
  unlinkSync(tmpPath);
18485
19860
  }
18486
19861
  }
19862
+ function removedIds(before, after) {
19863
+ const kept = /* @__PURE__ */ new Set();
19864
+ for (const item of after) {
19865
+ const id = item?.id;
19866
+ if (typeof id === "string") kept.add(id);
19867
+ }
19868
+ const removed = [];
19869
+ for (const item of before) {
19870
+ const id = item?.id;
19871
+ if (typeof id === "string" && !kept.has(id)) removed.push(id);
19872
+ }
19873
+ return removed;
19874
+ }
19875
+ function listIds(ids, limit = 10) {
19876
+ if (ids.length <= limit) return ids.join(", ");
19877
+ return `${ids.slice(0, limit).join(", ")}, and ${ids.length - limit} more`;
19878
+ }
19879
+ function describeDeletions(removedNodes, removedEdges, bundlePath) {
19880
+ const parts = [];
19881
+ if (removedNodes.length > 0) parts.push(`${removedNodes.length} node${removedNodes.length === 1 ? "" : "s"}`);
19882
+ if (removedEdges.length > 0) parts.push(`${removedEdges.length} edge${removedEdges.length === 1 ? "" : "s"}`);
19883
+ const lines = [
19884
+ `This restore would DELETE ${parts.join(" and ")} the hosted project currently has and ${bundlePath} does not. Nothing was sent.`
19885
+ ];
19886
+ if (removedNodes.length > 0) lines.push(` nodes: ${listIds(removedNodes)}`);
19887
+ if (removedEdges.length > 0) lines.push(` edges: ${listIds(removedEdges)}`);
19888
+ lines.push(
19889
+ `Usually this means the hosted project moved ahead of your local copy (someone edited it in the app) and the local bundle is the stale side \u2014 not that you meant to delete anything. Adopt the hosted state locally first: \`GET /api/graph/projects/<id>/export\` is what answers "what am I about to overwrite".`
19890
+ );
19891
+ lines.push(
19892
+ `If the deletions really are intended \u2014 undoing an earlier restore from a backup legitimately removes what that restore added \u2014 re-run with --allow-deletions.`
19893
+ );
19894
+ return lines.join("\n");
19895
+ }
19896
+ function describeQualityLoss(hostedQuality, qualityRoot, noQuality, foldNotice) {
19897
+ const findings = Array.isArray(hostedQuality.findings) ? hostedQuality.findings.length : 0;
19898
+ const assessments = Array.isArray(hostedQuality.assessments) ? hostedQuality.assessments.length : 0;
19899
+ const parts = [];
19900
+ if (findings > 0) parts.push(`${findings} open finding${findings === 1 ? "" : "s"}`);
19901
+ if (assessments > 0) parts.push(`${assessments} assessment${assessments === 1 ? "" : "s"}`);
19902
+ const held = parts.length > 0 ? parts.join(" and ") : "no findings or assessments";
19903
+ const lines = [
19904
+ `This restore would ERASE the hosted project's quality section (${held}). Nothing was sent.`
19905
+ ];
19906
+ if (noQuality) {
19907
+ lines.push(
19908
+ `You passed --no-quality, which says "do not send my quality data" \u2014 not "destroy what is already there". Those are different decisions, and this verb has no server-side undo, so the second one has to be typed: re-run with --allow-quality-loss as well if you really mean to wipe it.`
19909
+ );
19910
+ } else {
19911
+ if (foldNotice !== void 0) {
19912
+ lines.push("The outbound bundle has no section because the fold found nothing to build one from:");
19913
+ lines.push(` ${foldNotice}`);
19914
+ }
19915
+ if (!existsSync9(join8(qualityRoot, QUALITY_DIR))) {
19916
+ lines.push(
19917
+ `There is no ${QUALITY_DIR}/ under ${qualityRoot} at all. If this project's sidecars live somewhere else, point --root at that repo and the section is rebuilt rather than removed.`
19918
+ );
19919
+ }
19920
+ lines.push(`If the hosted section really is meant to go, re-run with --allow-quality-loss.`);
19921
+ }
19922
+ return lines.join("\n");
19923
+ }
18487
19924
  async function runRestore(options = {}) {
18488
19925
  const cwd = options.cwd ?? process.cwd();
18489
19926
  const env = options.env ?? process.env;
18490
19927
  const dryRun = options.dryRun ?? false;
18491
19928
  const allowHistoryLoss = options.allowHistoryLoss ?? false;
19929
+ const allowDeletions = options.allowDeletions ?? false;
19930
+ const noQuality = options.noQuality ?? false;
19931
+ const allowQualityLoss = options.allowQualityLoss ?? false;
18492
19932
  const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
18493
- const linkPath = join6(cwd, LINK_FILE2);
18494
- if (!existsSync6(linkPath)) {
19933
+ const linkPath = join8(cwd, LINK_FILE2);
19934
+ if (!existsSync9(linkPath)) {
18495
19935
  return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
18496
19936
  }
18497
19937
  let link;
18498
19938
  try {
18499
- link = JSON.parse(readFileSync8(linkPath, "utf8"));
19939
+ link = JSON.parse(readFileSync9(linkPath, "utf8"));
18500
19940
  } catch (e) {
18501
19941
  return fatalResult5(dryRun, `Could not parse ${LINK_FILE2}: ${e.message}`);
18502
19942
  }
@@ -18506,11 +19946,11 @@ async function runRestore(options = {}) {
18506
19946
  const encodedProjectId = encodeURIComponent(projectId);
18507
19947
  const token = env.ARKAIK_TOKEN;
18508
19948
  if (!token) return fatalResult5(dryRun, `ARKAIK_TOKEN is not set. Create a token at ${baseUrl}/settings/tokens and export it.`);
18509
- const bundlePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
18510
- if (!existsSync6(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
19949
+ const bundlePath = resolve9(cwd, options.path ?? DEFAULT_BUNDLE_PATH9);
19950
+ if (!existsSync9(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
18511
19951
  let localRaw;
18512
19952
  try {
18513
- localRaw = JSON.parse(readFileSync8(bundlePath, "utf8"));
19953
+ localRaw = JSON.parse(readFileSync9(bundlePath, "utf8"));
18514
19954
  } catch (e) {
18515
19955
  return fatalResult5(dryRun, `Could not parse ${bundlePath}: ${e.message}`);
18516
19956
  }
@@ -18520,6 +19960,22 @@ async function runRestore(options = {}) {
18520
19960
  const local = localRaw;
18521
19961
  const journalEvents = loadJournalEvents(local, bundlePath);
18522
19962
  const outboundBundle = { ...local, journal: journalEvents };
19963
+ const qualityRoot = resolveQualityRoot(cwd, bundlePath, options.root);
19964
+ let qualityFolded = false;
19965
+ let qualityNotice;
19966
+ if (noQuality) {
19967
+ delete outboundBundle.quality;
19968
+ qualityNotice = "Quality: deleted before sending (--no-quality)";
19969
+ } else {
19970
+ try {
19971
+ const fold = foldQualitySection(outboundBundle, qualityRoot, options.audit);
19972
+ qualityFolded = fold.folded;
19973
+ qualityNotice = fold.notice;
19974
+ } catch (e) {
19975
+ return fatalResult5(dryRun, e.message);
19976
+ }
19977
+ }
19978
+ const qualityFields = { qualityFolded, qualityNotice };
18523
19979
  const headers = { Authorization: `Bearer ${token}` };
18524
19980
  let version2;
18525
19981
  try {
@@ -18548,7 +20004,7 @@ async function runRestore(options = {}) {
18548
20004
  } catch (e) {
18549
20005
  return { ok: true, dryRun, bundlePath, requestSent: false, errorMessage: `Network error: ${e.message}` };
18550
20006
  }
18551
- return interpretPutResponse(res2, { dryRun, bundlePath, version: version2 });
20007
+ return interpretPutResponse(res2, { dryRun, bundlePath, version: version2, ...qualityFields });
18552
20008
  }
18553
20009
  let exported;
18554
20010
  try {
@@ -18575,23 +20031,34 @@ async function runRestore(options = {}) {
18575
20031
  if (journalEvents.length < hostedEventCount && !allowHistoryLoss) {
18576
20032
  return fatalResult5(
18577
20033
  dryRun,
18578
- `This restore would replace ${hostedEventCount} hosted journal events with ${journalEvents.length}. Nothing was sent. If that is intended, re-run with --allow-history-loss; otherwise check that ${journalPathFor(bundlePath)} exists and is current.`
20034
+ `This restore would replace ${hostedEventCount} hosted journal events with ${journalEvents.length}. Nothing was sent. If that is intended, re-run with --allow-history-loss; otherwise check that ${journalPathFor(bundlePath)} exists and is current.`,
20035
+ qualityFields
18579
20036
  );
18580
20037
  }
18581
- const backupDir = join6(cwd, "docs", "arkaik", ".backups");
20038
+ const hostedQuality = exportedBundle.quality;
20039
+ if (isQualitySection(hostedQuality) && !isQualitySection(outboundBundle.quality) && !allowQualityLoss) {
20040
+ return fatalResult5(dryRun, describeQualityLoss(hostedQuality, qualityRoot, noQuality, qualityNotice), qualityFields);
20041
+ }
20042
+ const removedNodes = removedIds(exportedBundle.nodes, Array.isArray(local.nodes) ? local.nodes : []);
20043
+ const removedEdges = removedIds(exportedBundle.edges, Array.isArray(local.edges) ? local.edges : []);
20044
+ if ((removedNodes.length > 0 || removedEdges.length > 0) && !allowDeletions) {
20045
+ return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath), qualityFields);
20046
+ }
20047
+ const backupDir = join8(cwd, "docs", "arkaik", ".backups");
18582
20048
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18583
- const backupPath = join6(backupDir, `${stamp}-bundle.json`);
20049
+ const backupPath = join8(backupDir, `${stamp}-bundle.json`);
18584
20050
  const backupContent = `${JSON.stringify(exported, null, 2)}
18585
20051
  `;
18586
20052
  try {
18587
- mkdirSync5(backupDir, { recursive: true });
20053
+ mkdirSync6(backupDir, { recursive: true });
18588
20054
  writeBackupFile(backupPath, backupContent);
18589
- JSON.parse(readFileSync8(backupPath, "utf8"));
20055
+ JSON.parse(readFileSync9(backupPath, "utf8"));
18590
20056
  } catch (e) {
18591
20057
  return fatalResult5(
18592
20058
  dryRun,
18593
20059
  `Could not write the pre-restore backup to ${backupPath}: ${e.message}
18594
- Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`
20060
+ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`,
20061
+ qualityFields
18595
20062
  );
18596
20063
  }
18597
20064
  let res;
@@ -18611,7 +20078,7 @@ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND
18611
20078
  errorMessage: `Network error: ${e.message}. Nothing was sent.${backupNoteFor(backupPath)}`
18612
20079
  };
18613
20080
  }
18614
- return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2 });
20081
+ return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2, ...qualityFields });
18615
20082
  }
18616
20083
  function printDelta(delta) {
18617
20084
  if (!delta) return;
@@ -18625,12 +20092,26 @@ function printDelta(delta) {
18625
20092
  console.log(
18626
20093
  ` events ${n("eventsBefore")} -> ${n("eventsAfter")} (+${n("eventsAdded")} -${n("eventsDropped")} ~${n("eventsChanged")}${delta.eventsMalformed ? `, ${delta.eventsMalformed} malformed` : ""})`
18627
20094
  );
20095
+ const count = (k) => typeof delta[k] === "number" ? delta[k] : 0;
20096
+ const nodesRemoved = count("nodesRemoved");
20097
+ const edgesRemoved = count("edgesRemoved");
20098
+ if (nodesRemoved > 0 || edgesRemoved > 0) {
20099
+ const parts = [];
20100
+ if (nodesRemoved > 0) parts.push(`${nodesRemoved} node${nodesRemoved === 1 ? "" : "s"}`);
20101
+ if (edgesRemoved > 0) parts.push(`${edgesRemoved} edge${edgesRemoved === 1 ? "" : "s"}`);
20102
+ console.log(
20103
+ ` WARNING: this DELETES ${parts.join(" and ")} from the hosted project. Bootstrap never deletes \u2014 check the hosted project has not moved ahead of your local bundle.`
20104
+ );
20105
+ }
18628
20106
  }
18629
20107
  function reportRestore(result) {
18630
20108
  if (!result.ok) fail10(`FATAL: ${result.fatal}`);
18631
20109
  if (result.backupPath) {
18632
20110
  console.log(`Backed up the current hosted project (snapshot + journal) to ${result.backupPath}`);
18633
20111
  }
20112
+ if (result.qualityNotice !== void 0) {
20113
+ console.log(result.qualityNotice);
20114
+ }
18634
20115
  if (!result.requestSent) {
18635
20116
  console.error(result.errorMessage ?? "Restore failed before a request could be sent.");
18636
20117
  process.exit(1);
@@ -18639,7 +20120,9 @@ function reportRestore(result) {
18639
20120
  if (result.dryRun) {
18640
20121
  console.log("[dry-run] server preview \u2014 nothing was written:");
18641
20122
  printDelta(result.delta);
18642
- console.log("Re-run without --dry-run to apply \u2014 that run takes the backup.");
20123
+ console.log(
20124
+ "Re-run without --dry-run to apply \u2014 that run takes the backup, and only that run checks the history, deletion and quality-loss guards (all three read the export, which a dry run never fetches)."
20125
+ );
18643
20126
  } else {
18644
20127
  console.log(`Restored. New version ${result.version}.`);
18645
20128
  printDelta(result.delta);
@@ -18659,6 +20142,11 @@ function reportRestore(result) {
18659
20142
  function runRestoreCli(argv) {
18660
20143
  let dryRun = false;
18661
20144
  let allowHistoryLoss = false;
20145
+ let allowDeletions = false;
20146
+ let noQuality = false;
20147
+ let allowQualityLoss = false;
20148
+ let audit;
20149
+ let root;
18662
20150
  let apiBase;
18663
20151
  const positionals = [];
18664
20152
  for (let i = 0; i < argv.length; i++) {
@@ -18671,6 +20159,24 @@ function runRestoreCli(argv) {
18671
20159
  dryRun = true;
18672
20160
  } else if (arg === "--allow-history-loss") {
18673
20161
  allowHistoryLoss = true;
20162
+ } else if (arg === "--allow-deletions") {
20163
+ allowDeletions = true;
20164
+ } else if (arg === "--no-quality") {
20165
+ noQuality = true;
20166
+ } else if (arg === "--allow-quality-loss") {
20167
+ allowQualityLoss = true;
20168
+ } else if (arg === "--audit") {
20169
+ const value = argv[++i];
20170
+ if (value === void 0) fail10(`Missing value for --audit
20171
+
20172
+ ${USAGE11}`);
20173
+ audit = value;
20174
+ } else if (arg === "--root") {
20175
+ const value = argv[++i];
20176
+ if (value === void 0) fail10(`Missing value for --root
20177
+
20178
+ ${USAGE11}`);
20179
+ root = value;
18674
20180
  } else if (arg === "--api") {
18675
20181
  const value = argv[++i];
18676
20182
  if (value === void 0) fail10(`Missing value for --api
@@ -18688,28 +20194,33 @@ ${USAGE11}`);
18688
20194
  if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
18689
20195
 
18690
20196
  ${USAGE11}`);
18691
- runRestore({ path: positionals[0], dryRun, allowHistoryLoss, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
20197
+ if (noQuality && audit !== void 0) {
20198
+ fail10(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
20199
+
20200
+ ${USAGE11}`);
20201
+ }
20202
+ runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, noQuality, allowQualityLoss, audit, root, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
18692
20203
  }
18693
20204
 
18694
20205
  // src/commands/bootstrap.ts
18695
20206
  import { spawnSync as spawnSync2 } from "node:child_process";
18696
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, renameSync, writeFileSync as writeFileSync12 } from "node:fs";
20207
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync, writeFileSync as writeFileSync13 } from "node:fs";
18697
20208
  import path5 from "node:path";
18698
20209
 
18699
20210
  // src/lib/bootstrap/corpus.ts
18700
20211
  import { spawnSync } from "node:child_process";
18701
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
20212
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
18702
20213
  import path2 from "node:path";
18703
20214
 
18704
20215
  // src/lib/bootstrap/paths.ts
18705
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "node:fs";
20216
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
18706
20217
  import path from "node:path";
18707
20218
  var BOOTSTRAP_ROOT = ".arkaik";
18708
20219
  var CORPUS_DIR = ".arkaik/corpus";
18709
20220
  var PLAN_DIR = ".arkaik/bootstrap";
18710
20221
  var FRAGMENTS_DIR = ".arkaik/bootstrap/fragments";
18711
20222
  var MANIFEST_FILE = ".arkaik/bootstrap/manifest.json";
18712
- var PROFILE_FILE = ".arkaik/bootstrap/profile.json";
20223
+ var PROFILE_FILE2 = ".arkaik/bootstrap/profile.json";
18713
20224
  var PRS_FILE = ".arkaik/corpus/prs.jsonl";
18714
20225
  var DOCS_FILE = ".arkaik/corpus/docs.json";
18715
20226
  var SURFACES_FILE = ".arkaik/corpus/surfaces.json";
@@ -18717,16 +20228,16 @@ function at(cwd, relative) {
18717
20228
  return path.join(cwd, relative);
18718
20229
  }
18719
20230
  function ensureDir(dirPath) {
18720
- mkdirSync6(dirPath, { recursive: true });
20231
+ mkdirSync7(dirPath, { recursive: true });
18721
20232
  }
18722
20233
  function ensureGitignored(cwd) {
18723
20234
  const file2 = path.join(cwd, ".gitignore");
18724
20235
  const line2 = `${BOOTSTRAP_ROOT}/`;
18725
- const current = existsSync7(file2) ? readFileSync9(file2, "utf8") : "";
20236
+ const current = existsSync10(file2) ? readFileSync10(file2, "utf8") : "";
18726
20237
  const ignored = current.split("\n").map((l) => l.trim()).some((l) => l === line2 || l === BOOTSTRAP_ROOT);
18727
20238
  if (ignored) return false;
18728
20239
  const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
18729
- writeFileSync9(file2, `${current}${prefix}${line2}
20240
+ writeFileSync10(file2, `${current}${prefix}${line2}
18730
20241
  `);
18731
20242
  return true;
18732
20243
  }
@@ -18823,7 +20334,7 @@ function fetchPrsViaGit(cwd) {
18823
20334
  function walk(root, cwd, out) {
18824
20335
  let entries;
18825
20336
  try {
18826
- entries = readdirSync(root, { withFileTypes: true });
20337
+ entries = readdirSync3(root, { withFileTypes: true });
18827
20338
  } catch {
18828
20339
  return;
18829
20340
  }
@@ -18841,7 +20352,7 @@ function listFiles(cwd) {
18841
20352
  }
18842
20353
  function buildDocsManifest(cwd, files) {
18843
20354
  return files.filter((f) => f.startsWith("docs/") && f.endsWith(".md")).map((f) => {
18844
- const text = readFileSync10(path2.join(cwd, f), "utf8");
20355
+ const text = readFileSync11(path2.join(cwd, f), "utf8");
18845
20356
  const heading = /^#\s+(.+)$/m.exec(text);
18846
20357
  return { path: f, title: heading ? heading[1].trim() : path2.basename(f, ".md") };
18847
20358
  });
@@ -18856,7 +20367,7 @@ function buildSurfaceInventory(files) {
18856
20367
  }
18857
20368
  function buildCorpus(options) {
18858
20369
  const { cwd } = options;
18859
- const raw = options.fromJson ? JSON.parse(readFileSync10(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
20370
+ const raw = options.fromJson ? JSON.parse(readFileSync11(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
18860
20371
  let prs = normalizePrs(raw);
18861
20372
  let sinceDroppedUndated = 0;
18862
20373
  if (options.since) {
@@ -18877,21 +20388,21 @@ function buildCorpus(options) {
18877
20388
  const docs = buildDocsManifest(cwd, files);
18878
20389
  const surfaces = buildSurfaceInventory(files);
18879
20390
  ensureDir(at(cwd, CORPUS_DIR));
18880
- writeFileSync10(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
18881
- writeFileSync10(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
20391
+ writeFileSync11(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
20392
+ writeFileSync11(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
18882
20393
  `);
18883
- writeFileSync10(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
20394
+ writeFileSync11(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
18884
20395
  `);
18885
20396
  return { prs: prs.length, docs: docs.length, surfaces: surfaces.length, sinceDroppedUndated };
18886
20397
  }
18887
20398
  function readCorpusPrs(cwd) {
18888
20399
  const file2 = at(cwd, PRS_FILE);
18889
- if (!existsSync8(file2)) return [];
18890
- return readFileSync10(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
20400
+ if (!existsSync11(file2)) return [];
20401
+ return readFileSync11(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
18891
20402
  }
18892
20403
 
18893
20404
  // src/lib/bootstrap/fragments.ts
18894
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
20405
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
18895
20406
  import path3 from "node:path";
18896
20407
  function isArrayOfObjects(value) {
18897
20408
  return value === void 0 || Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null && !Array.isArray(v));
@@ -18917,13 +20428,13 @@ function loadFragments(cwd, manifest) {
18917
20428
  problems.push({ unit: String(unit2.id), message: err instanceof Error ? err.message : String(err) });
18918
20429
  continue;
18919
20430
  }
18920
- if (!existsSync9(file2)) {
20431
+ if (!existsSync12(file2)) {
18921
20432
  missing.push(unit2.id);
18922
20433
  continue;
18923
20434
  }
18924
20435
  let parsed;
18925
20436
  try {
18926
- parsed = JSON.parse(readFileSync11(file2, "utf8"));
20437
+ parsed = JSON.parse(readFileSync12(file2, "utf8"));
18927
20438
  } catch (err) {
18928
20439
  problems.push({ unit: unit2.id, message: `not valid JSON: ${err instanceof Error ? err.message : "parse error"}` });
18929
20440
  continue;
@@ -18960,7 +20471,7 @@ function renderIndex(bundle) {
18960
20471
  }
18961
20472
 
18962
20473
  // src/lib/bootstrap/manifest.ts
18963
- import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "node:fs";
20474
+ import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "node:fs";
18964
20475
  import path4 from "node:path";
18965
20476
 
18966
20477
  // src/lib/bootstrap/era-window.ts
@@ -19068,19 +20579,19 @@ function assertValidProfile(profile) {
19068
20579
 
19069
20580
  // src/lib/bootstrap/manifest.ts
19070
20581
  function readProfile(cwd) {
19071
- const file2 = at(cwd, PROFILE_FILE);
19072
- if (!existsSync10(file2)) return null;
20582
+ const file2 = at(cwd, PROFILE_FILE2);
20583
+ if (!existsSync13(file2)) return null;
19073
20584
  try {
19074
- return JSON.parse(readFileSync12(file2, "utf8"));
20585
+ return JSON.parse(readFileSync13(file2, "utf8"));
19075
20586
  } catch (err) {
19076
- throw new Error(`cannot read ${PROFILE_FILE}: ${err instanceof Error ? err.message : String(err)}`);
20587
+ throw new Error(`cannot read ${PROFILE_FILE2}: ${err instanceof Error ? err.message : String(err)}`);
19077
20588
  }
19078
20589
  }
19079
20590
  function readManifest(cwd) {
19080
20591
  const file2 = at(cwd, MANIFEST_FILE);
19081
- if (!existsSync10(file2)) return null;
20592
+ if (!existsSync13(file2)) return null;
19082
20593
  try {
19083
- return JSON.parse(readFileSync12(file2, "utf8"));
20594
+ return JSON.parse(readFileSync13(file2, "utf8"));
19084
20595
  } catch (err) {
19085
20596
  throw new Error(`cannot read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
19086
20597
  }
@@ -19088,15 +20599,15 @@ function readManifest(cwd) {
19088
20599
  function writeManifest(cwd, manifest) {
19089
20600
  ensureDir(at(cwd, PLAN_DIR));
19090
20601
  ensureDir(at(cwd, FRAGMENTS_DIR));
19091
- writeFileSync11(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
20602
+ writeFileSync12(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
19092
20603
  `);
19093
20604
  }
19094
20605
  function detectMode(cwd, bundlePath) {
19095
20606
  const file2 = path4.resolve(cwd, bundlePath);
19096
- if (!existsSync10(file2)) return "greenfield";
20607
+ if (!existsSync13(file2)) return "greenfield";
19097
20608
  let parsed;
19098
20609
  try {
19099
- parsed = JSON.parse(readFileSync12(file2, "utf8"));
20610
+ parsed = JSON.parse(readFileSync13(file2, "utf8"));
19100
20611
  } catch (err) {
19101
20612
  throw new Error(`cannot read bundle at ${bundlePath}: ${err instanceof Error ? err.message : String(err)}`);
19102
20613
  }
@@ -19320,7 +20831,7 @@ function mergeJournal(base, fresh) {
19320
20831
  }
19321
20832
 
19322
20833
  // src/lib/bootstrap/merge.ts
19323
- function asArray(value) {
20834
+ function asArray2(value) {
19324
20835
  return Array.isArray(value) ? value : [];
19325
20836
  }
19326
20837
  function mergeFragments(input) {
@@ -19328,14 +20839,14 @@ function mergeFragments(input) {
19328
20839
  const projectId = String(input.base.project?.id ?? "");
19329
20840
  const nodes = /* @__PURE__ */ new Map();
19330
20841
  const nodeOrigin = /* @__PURE__ */ new Map();
19331
- for (const node of asArray(input.base.nodes)) {
20842
+ for (const node of asArray2(input.base.nodes)) {
19332
20843
  const id = String(node.id);
19333
20844
  nodes.set(id, { ...node });
19334
20845
  nodeOrigin.set(id, "(already in the bundle)");
19335
20846
  }
19336
20847
  const edges = /* @__PURE__ */ new Map();
19337
20848
  const edgeOrigin = /* @__PURE__ */ new Map();
19338
- for (const edge of asArray(input.base.edges)) {
20849
+ for (const edge of asArray2(input.base.edges)) {
19339
20850
  const id = String(edge.id ?? edgeId(String(edge.source_id), String(edge.target_id)));
19340
20851
  edges.set(id, { ...edge });
19341
20852
  edgeOrigin.set(id, "(already in the bundle)");
@@ -19549,7 +21060,7 @@ function mergeFragments(input) {
19549
21060
  }
19550
21061
 
19551
21062
  // src/lib/bootstrap/slice.ts
19552
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
21063
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
19553
21064
 
19554
21065
  // src/lib/bootstrap/body-budget.ts
19555
21066
  var LAB_NOTE_HEADING_RE = /^##\s+Lab Note.*$/m;
@@ -19591,8 +21102,8 @@ function boundBody(body) {
19591
21102
 
19592
21103
  // src/lib/bootstrap/slice.ts
19593
21104
  function readJsonArray(file2) {
19594
- if (!existsSync11(file2)) return [];
19595
- const parsed = JSON.parse(readFileSync13(file2, "utf8"));
21105
+ if (!existsSync14(file2)) return [];
21106
+ const parsed = JSON.parse(readFileSync14(file2, "utf8"));
19596
21107
  return Array.isArray(parsed) ? parsed : [];
19597
21108
  }
19598
21109
  function toPosix(value) {
@@ -19613,7 +21124,7 @@ function eraWindows(cwd, slugs) {
19613
21124
  const era = bySlug.get(slug);
19614
21125
  if (!era) {
19615
21126
  throw new Error(
19616
- `era "${slug}" is not declared in ${PROFILE_FILE}'s "eras" list, but a work unit's slice references it. profile.json may have been edited (or the era removed) since \`arkaik bootstrap plan\` last ran. Restore the era in profile.json and re-run \`arkaik bootstrap plan\`, or reconcile the manifest by hand.`
21127
+ `era "${slug}" is not declared in ${PROFILE_FILE2}'s "eras" list, but a work unit's slice references it. profile.json may have been edited (or the era removed) since \`arkaik bootstrap plan\` last ran. Restore the era in profile.json and re-run \`arkaik bootstrap plan\`, or reconcile the manifest by hand.`
19617
21128
  );
19618
21129
  }
19619
21130
  assertEraWindow(era);
@@ -19702,7 +21213,7 @@ ${usage}`);
19702
21213
  }
19703
21214
  function writeFileAtomic(filePath, content) {
19704
21215
  const tmpPath = `${filePath}.tmp-${process.pid}`;
19705
- writeFileSync12(tmpPath, content);
21216
+ writeFileSync13(tmpPath, content);
19706
21217
  renameSync(tmpPath, filePath);
19707
21218
  }
19708
21219
  function runCorpus(argv) {
@@ -19737,7 +21248,7 @@ ${CORPUS_USAGE}`);
19737
21248
  ${CORPUS_USAGE}`);
19738
21249
  }
19739
21250
  }
19740
- if (!existsSync12(path5.join(cwd, ".git"))) {
21251
+ if (!existsSync15(path5.join(cwd, ".git"))) {
19741
21252
  fail11("`arkaik bootstrap corpus` must run from the repository root (no .git here).");
19742
21253
  }
19743
21254
  try {
@@ -19799,7 +21310,7 @@ ${PLAN_USAGE}`);
19799
21310
 
19800
21311
  ${PLAN_USAGE}`);
19801
21312
  }
19802
- if (!existsSync12(path5.join(cwd, ".git"))) {
21313
+ if (!existsSync15(path5.join(cwd, ".git"))) {
19803
21314
  fail11("`arkaik bootstrap plan` must run from the repository root (no .git here).");
19804
21315
  }
19805
21316
  try {
@@ -19938,7 +21449,7 @@ ${MERGE_USAGE}`);
19938
21449
  }
19939
21450
  try {
19940
21451
  const bundlePath = path5.resolve(cwd, manifest.bundle);
19941
- const base = existsSync12(bundlePath) ? readBundle(bundlePath) : {
21452
+ const base = existsSync15(bundlePath) ? readBundle(bundlePath) : {
19942
21453
  schema_version: 3,
19943
21454
  project: {
19944
21455
  id: path5.basename(cwd),
@@ -19974,7 +21485,7 @@ ${MERGE_USAGE}`);
19974
21485
  const journalPath = journalPathFor(bundlePath);
19975
21486
  const journalText = result.journal.map((e) => JSON.stringify(e)).join("\n") + (result.journal.length ? "\n" : "");
19976
21487
  if (!dryRun) {
19977
- mkdirSync7(path5.dirname(bundlePath), { recursive: true });
21488
+ mkdirSync8(path5.dirname(bundlePath), { recursive: true });
19978
21489
  writeFileAtomic(bundlePath, serialized);
19979
21490
  writeFileAtomic(journalPath, journalText);
19980
21491
  }
@@ -20023,8 +21534,1039 @@ ${USAGE12}`);
20023
21534
  }
20024
21535
  }
20025
21536
 
21537
+ // ../schema/src/cli/kritik-overlay.ts
21538
+ var ANCHOR_KEYS = ["l0", "l1", "l2", "l3", "l4"];
21539
+ var CRITERION_TEMPLATE = {
21540
+ id: "X-01",
21541
+ domain: "ARC",
21542
+ subcategory: "conventions",
21543
+ name: "Short criterion name",
21544
+ question: "The criterion as one question an auditor can actually answer?",
21545
+ definition: "What good looks like on this surface, concretely.",
21546
+ rationale: "Why this matters for this product in particular.",
21547
+ applies_to: ["web"],
21548
+ level_anchors: {
21549
+ l0: "Not addressed at all.",
21550
+ l1: "Addressed accidentally or in one spot; no visible intent.",
21551
+ l2: "Deliberately addressed; visible intent; known gaps.",
21552
+ l3: "Systematic across the surface; tested or reviewed.",
21553
+ l4: "Enforced by automation or a CI gate; drift is detected, not hoped against."
21554
+ },
21555
+ default_impact: 3,
21556
+ weight: 1,
21557
+ references: [],
21558
+ checklist: ["The grep, file, or flow an auditor should actually run."],
21559
+ signals: ["A check that can run between audits and fail mechanically."],
21560
+ remediation: "The typical fix path.",
21561
+ issue: {
21562
+ title_template: "[Quality] X-01 <name> at level {observed_level} on {surface} (target {target_level})",
21563
+ labels: ["quality"],
21564
+ body_skeleton: "## Quality finding: X-01 <name>\n\n**Surface:** {surface}\n**Observed level:** {observed_level}\n**Target level:** {target_level}\n\n### Evidence\n{evidence_bullets_with_file_paths}\n\n### Remediation\n- [ ] {remediation_step_1}\n\n### Acceptance criteria\n- [ ] Target anchor holds: {target_anchor_text}"
21565
+ }
21566
+ };
21567
+ function buildCriterion(draft) {
21568
+ for (const [label, value] of [
21569
+ ["id", draft.id],
21570
+ ["domain", draft.domain],
21571
+ ["name", draft.name],
21572
+ ["question", draft.question]
21573
+ ]) {
21574
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`--${label} is required`);
21575
+ }
21576
+ if (draft.appliesTo.length === 0) throw new Error(`--applies-to is required \u2014 a criterion applies to at least one surface`);
21577
+ const missing = ANCHOR_KEYS.filter((key) => !draft.anchors[key]);
21578
+ if (missing.length > 0) {
21579
+ throw new Error(
21580
+ `missing anchors ${missing.join(", ")}.
21581
+ All five are required: a criterion without observable descriptions of each level cannot be scored consistently,
21582
+ and an inconsistently scored criterion makes its whole row incomparable.`
21583
+ );
21584
+ }
21585
+ const weight = draft.weight ?? 1;
21586
+ if (!Number.isInteger(weight) || weight < 1 || weight > 3) throw new Error(`--weight must be 1, 2 or 3`);
21587
+ const impact = draft.impact ?? 3;
21588
+ if (!Number.isInteger(impact) || impact < 1 || impact > 5) throw new Error(`--impact must be 1-5`);
21589
+ const { id, name } = draft;
21590
+ return {
21591
+ id,
21592
+ domain: draft.domain,
21593
+ ...draft.subcategory ? { subcategory: draft.subcategory } : {},
21594
+ name,
21595
+ question: draft.question,
21596
+ ...draft.definition ? { definition: draft.definition } : {},
21597
+ ...draft.rationale ? { rationale: draft.rationale } : {},
21598
+ applies_to: draft.appliesTo,
21599
+ level_anchors: draft.anchors,
21600
+ default_impact: impact,
21601
+ weight,
21602
+ references: [],
21603
+ checklist: draft.checklist ?? [],
21604
+ signals: draft.signals ?? [],
21605
+ ...draft.remediation ? { remediation: draft.remediation } : {},
21606
+ issue: {
21607
+ title_template: `[Quality] ${id} ${name} at level {observed_level} on {surface} (target {target_level})`,
21608
+ labels: draft.labels && draft.labels.length > 0 ? draft.labels : ["quality"],
21609
+ body_skeleton: `## Quality finding: ${id} ${name}
21610
+
21611
+ **Surface:** {surface}
21612
+ **Observed level:** {observed_level}
21613
+ **Target level:** {target_level}
21614
+ **Severity seed:** impact {impact} x likelihood {likelihood}
21615
+
21616
+ ### Evidence
21617
+ {evidence_bullets_with_file_paths}
21618
+
21619
+ ### Risk
21620
+ {risk_narrative}
21621
+
21622
+ ### Remediation
21623
+ - [ ] {remediation_step_1}
21624
+
21625
+ ### Acceptance criteria
21626
+ - [ ] Target anchor holds: {target_anchor_text}`
21627
+ }
21628
+ };
21629
+ }
21630
+ function addCriterionToOverlay(root, pack, criterion, options = {}) {
21631
+ if (typeof criterion.id !== "string" || criterion.id === "") throw new Error(`the criterion has no id`);
21632
+ if ((pack.criteria ?? []).some((c) => c.id === criterion.id) && !options.force) {
21633
+ throw new Error(
21634
+ `"${criterion.id}" is already a pack criterion.
21635
+ Overriding it changes what every score recorded against that id means.
21636
+ Use a project-reserved id (X-01, X-02, \u2026) instead, or pass --force if the override is deliberate.`
21637
+ );
21638
+ }
21639
+ const path6 = overlayPath(root);
21640
+ const overlay = loadOverlay(root) ?? { extends: pack.version, criteria: [] };
21641
+ overlay.criteria = Array.isArray(overlay.criteria) ? overlay.criteria : [];
21642
+ const at2 = overlay.criteria.findIndex((c) => c?.id === criterion.id);
21643
+ if (at2 >= 0 && !options.force) {
21644
+ throw new Error(`"${criterion.id}" is already in the overlay \u2014 pass --force to replace it`);
21645
+ }
21646
+ if (at2 >= 0) overlay.criteria[at2] = criterion;
21647
+ else overlay.criteria.push(criterion);
21648
+ const domainCode = criterion.domain;
21649
+ const knownDomain = (pack.domains ?? []).some((d) => d.code === domainCode) || (overlay.domains ?? []).some((d) => d.code === domainCode);
21650
+ let addedDomain;
21651
+ if (!knownDomain) {
21652
+ if (!options.domainName) {
21653
+ throw new Error(
21654
+ `"${domainCode}" is not a pack domain and the overlay does not define it.
21655
+ Pass --domain-name "<display name>" to define it, or use an existing domain code.`
21656
+ );
21657
+ }
21658
+ overlay.domains = [...overlay.domains ?? [], { code: domainCode, name: options.domainName }];
21659
+ addedDomain = domainCode;
21660
+ }
21661
+ writeJson(path6, overlay);
21662
+ return { path: path6, overlay, replaced: at2 >= 0, ...addedDomain !== void 0 ? { addedDomain } : {} };
21663
+ }
21664
+
21665
+ // src/commands/kritik.ts
21666
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
21667
+ var USAGE13 = `arkaik kritik <subcommand> [options]
21668
+
21669
+ Audit this product's quality with the Kritik framework: a maturity level per
21670
+ criterion per surface backed by evidence, findings carrying risk and cost, and
21671
+ a comparative matrix whose caps stop one open Critical being averaged into a B.
21672
+
21673
+ Subcommands:
21674
+ profile Pick this project's surfaces (writes docs/quality/profile.json).
21675
+ score <c> <s> <lvl> Record one maturity level, with its evidence.
21676
+ finding open ... Open a finding on a (criterion x surface) cell.
21677
+ finding resolve <id> Close it because the fix merged.
21678
+ finding accept <id> Accept it as a known, owned risk.
21679
+ matrix [audit-id] Roll an audit up (writes matrix.json).
21680
+ signals The signal pack, and what has tripped since the last audit.
21681
+ regressions What got worse between two audits.
21682
+ issue <criterion> Print the prefilled GitHub issue skeleton.
21683
+ criterion add ... Add a project-specific criterion to the overlay.
21684
+
21685
+ Common options:
21686
+ --root <dir> Repo root holding docs/quality/ (default: the current directory).
21687
+ --actor <name> Who is writing (default: ${KRITIK_ACTOR}). Recorded on every quality.* event.
21688
+ --bundle <path> The Arkaik bundle whose journal receives events
21689
+ (default: docs/arkaik/bundle.json; skipped when absent).
21690
+ --no-journal Write the audit files only; append no journal events.
21691
+ -h, --help Show this help, or a subcommand's.
21692
+
21693
+ Run "arkaik kritik <subcommand> --help" for the flags each one takes.`;
21694
+ var SCORE_USAGE = `arkaik kritik score <criterion> <surface> <level> --evidence <text|@file> [options]
21695
+
21696
+ Record one (criterion x surface) maturity level. A cell holds exactly one level,
21697
+ so re-scoring replaces in place \u2014 the correction reads as a correction.
21698
+
21699
+ Arguments:
21700
+ criterion A criterion id from the pack or this project's overlay (e.g. SEC-01).
21701
+ surface A surface id declared in docs/quality/profile.json.
21702
+ level 0-4. Absent from the file means N/A; 0 means "not addressed".
21703
+
21704
+ Options:
21705
+ --evidence <e> file:line / config citations. \`@path\` reads them from a file.
21706
+ Required: a score without a citation is an opinion.
21707
+ --audit <id> The audit run (default: the newest, or the current YYYY-MM).
21708
+ --commit <sha> The commit the evidence is pinned to.
21709
+
21710
+ Writes docs/quality/audits/<id>/scores.json`;
21711
+ var FINDING_USAGE = `arkaik kritik finding <open|resolve|accept> [options]
21712
+
21713
+ open <criterion> <surface> --title <t> --impact <1-5> --likelihood <1-5>
21714
+ --cost <S|M|L|XL> --evidence <text|@file> [--detail <d>] [--remediation <r>]
21715
+ [--nodes <id,id>] [--issue-url <u>] [--id <finding-id>] [--audit <id>]
21716
+
21717
+ Opens a finding and appends quality.finding.opened. Severity and priority are
21718
+ printed, never stored \u2014 they are derived from impact x likelihood and cost, so
21719
+ they can never drift from the numbers behind them. The surface may be
21720
+ "${CROSS_SURFACE_ID}" for a defect that belongs to the contract between surfaces.
21721
+
21722
+ resolve <finding-id> [--by <pr-or-commit-url>]
21723
+
21724
+ Marks it resolved and appends quality.finding.resolved. Searched across every
21725
+ audit: a finding opened in 2026-08 is routinely fixed during 2026-09.
21726
+
21727
+ accept <finding-id> --note <why>
21728
+
21729
+ Records it as a known, owned risk. The note is required \u2014 an accepted risk is a
21730
+ decision and reads like one. No journal event: acceptance is a state of the
21731
+ finding, not something that happened to the product.`;
21732
+ var MATRIX_USAGE = `arkaik kritik matrix [audit-id] [--json] [--record]
21733
+
21734
+ Roll one audit up into its comparative matrix and write matrix.json \u2014 the only
21735
+ thing that may write that file.
21736
+
21737
+ audit-id The audit under docs/quality/audits/ (default: the newest).
21738
+ --json Print matrix.json instead of the markdown table.
21739
+ --record Also append quality.audit.completed, carrying these scores
21740
+ and counts. Do this once per finished audit.`;
21741
+ var SIGNALS_USAGE = `arkaik kritik signals [--surface <s>] [--criterion <c>] [--domain <CODE>] [--json]
21742
+ arkaik kritik signals --trip <criterion> --surface <s> --signal <index|text> [--detail <d>]
21743
+
21744
+ Signals are the cheap checks between full audits: a grep that must return
21745
+ nothing, a CI job that must exist. They are statements to check, not commands to
21746
+ run \u2014 the pack spans greps, CI introspection and database queries, so this
21747
+ prints the run sheet rather than pretending to execute it.
21748
+
21749
+ Exits 1 when anything has tripped since the last recorded audit, which is what
21750
+ makes it usable as a CI step.
21751
+
21752
+ --trip Record one as tripped: appends quality.signal.tripped.
21753
+ --signal takes the row's index from the run sheet, or the text.
21754
+ --json The full run sheet as JSON (what an agent should read).`;
21755
+ var REGRESSIONS_USAGE = `arkaik kritik regressions [--from <audit>] [--to <audit>] [--record] [--json]
21756
+
21757
+ What got worse between two audits: a cell whose maturity dropped, a cell that
21758
+ gained an open Critical or High finding, a finding that was resolved and is open
21759
+ again. Cells scored in only one of the two audits are not compared \u2014 a
21760
+ half-finished audit is not a regression.
21761
+
21762
+ Exits 1 when anything regressed, which is what makes it usable as a CI step or a
21763
+ scheduled routine.
21764
+
21765
+ --from <audit> The older reading (default: the audit before --to).
21766
+ --to <audit> The newer reading (default: the newest on disk).
21767
+ --record Append one quality.signal.tripped per regression.
21768
+ --json The full list as JSON.`;
21769
+ var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
21770
+
21771
+ Print the criterion's GitHub issue skeleton, filled as far as what we know
21772
+ allows. Placeholders we cannot fill are left standing \u2014 a skeleton is a form to
21773
+ finish, and an empty "### Risk" reads as "no risk" where {risk_narrative} reads
21774
+ as "your turn".
21775
+
21776
+ --level <n> The observed maturity, so the anchors fill in.
21777
+ --finding <id> Fill the risk numbers, evidence and narrative from a finding.
21778
+ This is the P0/P1 path: the issue exists because a defect does.`;
21779
+ var CRITERION_USAGE = `arkaik kritik criterion add --id <ID> --domain <CODE> --name <t> --question <q> \\
21780
+ --applies-to <s1,s2> --anchor l0=<text> ... --anchor l4=<text> [options]
21781
+
21782
+ Add a criterion the pack does not have to this project's overlay
21783
+ (docs/quality/criteria.custom.json). A pack upgrade never touches it.
21784
+
21785
+ --id A project-reserved id, e.g. X-01. Never a pack id.
21786
+ --domain Owning domain code \u2014 an existing one (SEC, PRV, \u2026) or your own.
21787
+ --domain-name Display name, required only when --domain is a new code.
21788
+ --anchor lN=<t> What each level looks like HERE. All five are required: a
21789
+ criterion scored inconsistently makes its whole row incomparable.
21790
+ --weight <1-3> Weight in the domain roll-up (default 1).
21791
+ --impact <1-5> Seeds finding severity (default 3).
21792
+ --signal <s> A mechanically checkable hook; repeatable.
21793
+ --check <s> A concrete audit step; repeatable.
21794
+ --label <s> An issue label; repeatable (default: quality).
21795
+ --from <file> Read a complete criterion from JSON instead of these flags.
21796
+ --template Print a starting-point criterion and exit.
21797
+ --force Replace an existing criterion with this id.`;
21798
+ function fail12(message) {
21799
+ console.error(message);
21800
+ process.exit(1);
21801
+ }
21802
+ function takeCommon(args) {
21803
+ const rest = [];
21804
+ const common = { root: process.cwd(), actor: KRITIK_ACTOR, journal: true };
21805
+ for (let i = 0; i < args.length; i++) {
21806
+ const arg = args[i];
21807
+ if (arg === "--root") common.root = args[++i] ?? common.root;
21808
+ else if (arg === "--actor") common.actor = args[++i] ?? common.actor;
21809
+ else if (arg === "--bundle") common.bundlePath = args[++i];
21810
+ else if (arg === "--no-journal") common.journal = false;
21811
+ else rest.push(arg);
21812
+ }
21813
+ return { rest, common };
21814
+ }
21815
+ function collect(args, repeatable = [], boolean4 = []) {
21816
+ const single = {};
21817
+ const many = Object.fromEntries(repeatable.map((key) => [key, []]));
21818
+ const flags = /* @__PURE__ */ new Set();
21819
+ const positionals = [];
21820
+ for (let i = 0; i < args.length; i++) {
21821
+ const arg = args[i];
21822
+ if (!arg.startsWith("--")) {
21823
+ positionals.push(arg);
21824
+ continue;
21825
+ }
21826
+ const key = arg.slice(2);
21827
+ if (boolean4.includes(key) || key === "help") {
21828
+ flags.add(key);
21829
+ continue;
21830
+ }
21831
+ const value = args[++i];
21832
+ if (value === void 0) fail12(`kritik: --${key} needs a value`);
21833
+ if (key in many) many[key].push(value);
21834
+ else single[key] = value;
21835
+ }
21836
+ return { single, many, flags, positionals };
21837
+ }
21838
+ function textOrFile(value) {
21839
+ if (!value.startsWith("@")) return value;
21840
+ const path6 = value.slice(1);
21841
+ if (!existsSync16(path6)) fail12(`kritik: no file at ${path6}`);
21842
+ return readFileSync15(path6, "utf8").trim();
21843
+ }
21844
+ function currentAuditId() {
21845
+ const now = /* @__PURE__ */ new Date();
21846
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
21847
+ }
21848
+ function auditForWrite(root, requested) {
21849
+ if (requested !== void 0) return requested;
21850
+ const existing = listAuditIds(root);
21851
+ return existing.length > 0 ? existing[existing.length - 1] : currentAuditId();
21852
+ }
21853
+ function loadLibraryOrFail(root) {
21854
+ try {
21855
+ return loadKritikLibrary(root).library;
21856
+ } catch (error51) {
21857
+ return fail12(`kritik: ${error51.message}`);
21858
+ }
21859
+ }
21860
+ function profileOrFail(root) {
21861
+ try {
21862
+ return requireProfile(root);
21863
+ } catch (error51) {
21864
+ return fail12(`kritik: ${error51.message}`);
21865
+ }
21866
+ }
21867
+ function criterionOrFail(library, criterionId) {
21868
+ const criterion = (library.criteria ?? []).find((candidate) => candidate.id === criterionId);
21869
+ if (!criterion) {
21870
+ const domain2 = criterionId.split("-")[0];
21871
+ const siblings = (library.criteria ?? []).filter((c) => c.domain === domain2).map((c) => c.id);
21872
+ return fail12(
21873
+ `kritik: no criterion "${criterionId}" in the pack or this project's overlay.
21874
+ ` + (siblings.length > 0 ? `Criteria in ${domain2}: ${siblings.join(", ")}` : `Domains: ${(library.domains ?? []).map((d) => d.code).join(", ")}`)
21875
+ );
21876
+ }
21877
+ if (typeof criterion.superseded_by === "string") {
21878
+ return fail12(
21879
+ `kritik: criterion "${criterionId}" is retired \u2014 superseded by "${criterion.superseded_by}".
21880
+ Score that one instead; the old id is kept only so past audits still mean what they meant.`
21881
+ );
21882
+ }
21883
+ return criterion;
21884
+ }
21885
+ function surfaceOrFail(profile, surface, options = {}) {
21886
+ if (surface === CROSS_SURFACE_ID) {
21887
+ if (options.allowCrossSurface) return surface;
21888
+ return fail12(
21889
+ `kritik: "${CROSS_SURFACE_ID}" is a findings-only lens \u2014 it carries no matrix column, so a score there would render nowhere.`
21890
+ );
21891
+ }
21892
+ const declared = (profile.surfaces ?? []).map((s) => s.id);
21893
+ if (!declared.includes(surface)) {
21894
+ return fail12(
21895
+ `kritik: surface "${surface}" is not declared in ${profilePath(".")}.
21896
+ This project's surfaces: ${declared.join(", ") || "(none \u2014 run `arkaik kritik profile` first)"}`
21897
+ );
21898
+ }
21899
+ return surface;
21900
+ }
21901
+ function intOrFail(label, value, min, max) {
21902
+ if (value === void 0) fail12(`kritik: ${label} is required
21903
+ `);
21904
+ const parsed = Number(value);
21905
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
21906
+ fail12(`kritik: ${label} must be an integer ${min}-${max} (got "${value}")`);
21907
+ }
21908
+ return parsed;
21909
+ }
21910
+ function reportJournal(root, inputs, common) {
21911
+ if (!common.journal) {
21912
+ console.log(` journal: skipped (--no-journal)`);
21913
+ return;
21914
+ }
21915
+ const journal = resolveJournal(root, common.bundlePath);
21916
+ if (!journal.present) {
21917
+ console.log(` journal: none at ${journal.bundlePath} \u2014 Kritik does not need one, so nothing was appended`);
21918
+ return;
21919
+ }
21920
+ const written = appendQualityEvents(root, inputs, { actor: common.actor, bundlePath: common.bundlePath });
21921
+ if (written.baseline !== void 0) {
21922
+ console.log(` journal: adopted ${written.baseline.node_ids?.length ?? 0} pre-existing nodes first`);
21923
+ }
21924
+ console.log(` journal: ${written.events.map((e) => e.type).join(", ")} -> ${written.journalPath}`);
21925
+ }
21926
+ var PROFILE_USAGE = `arkaik kritik profile --surface <id>[:<title>[:<platform>]] [...] [--weight <CODE>=<n>] [--force]
21927
+
21928
+ Pick this project's surfaces \u2014 the one decision everything downstream is shaped
21929
+ by. Each criterion's applies_to intersects this list to produce the audit's
21930
+ cells, and the matrix has exactly these columns.
21931
+
21932
+ --surface id[:title[:platform]]. \`platform\` (web|ios|android) is the
21933
+ optional bridge to the product map, for surfaces that ship
21934
+ views. A database contract or a CLI simply has none.
21935
+ --weight CODE=n How hard this product is graded on a domain (default 1).
21936
+ --force Overwrite an existing profile. Changing the surface list
21937
+ invalidates every score recorded against the old one.
21938
+
21939
+ Writes docs/quality/profile.json`;
21940
+ function runProfile(args, common) {
21941
+ const { many, flags } = collect(args, ["surface", "weight"], ["force"]);
21942
+ if (flags.has("help")) {
21943
+ console.log(PROFILE_USAGE);
21944
+ process.exit(0);
21945
+ }
21946
+ if (many.surface.length === 0) fail12(`kritik: at least one --surface is required
21947
+
21948
+ ${PROFILE_USAGE}`);
21949
+ const surfaces = [];
21950
+ const seen = /* @__PURE__ */ new Set();
21951
+ for (const spec of many.surface) {
21952
+ let surface;
21953
+ try {
21954
+ surface = parseSurfaceSpec(spec);
21955
+ } catch (error51) {
21956
+ return fail12(`kritik: ${error51.message}`);
21957
+ }
21958
+ if (seen.has(surface.id)) fail12(`kritik: duplicate surface id "${surface.id}"`);
21959
+ seen.add(surface.id);
21960
+ surfaces.push(surface);
21961
+ }
21962
+ const weights = {};
21963
+ for (const spec of many.weight) {
21964
+ const at2 = spec.indexOf("=");
21965
+ if (at2 <= 0) fail12(`kritik: --weight wants CODE=number (got "${spec}")`);
21966
+ const code = spec.slice(0, at2).trim();
21967
+ const value = Number(spec.slice(at2 + 1));
21968
+ if (!Number.isFinite(value) || value <= 0) fail12(`kritik: weight for "${code}" must be a positive number`);
21969
+ weights[code] = value;
21970
+ }
21971
+ const path6 = profilePath(common.root);
21972
+ if (existsSync16(path6) && !flags.has("force")) {
21973
+ fail12(
21974
+ `kritik: ${path6} already exists.
21975
+ Changing the surface list invalidates every score recorded against the old one, so this refuses by default.
21976
+ Pass --force if that is genuinely what you want.`
21977
+ );
21978
+ }
21979
+ const profile = Object.keys(weights).length > 0 ? { surfaces, domain_weights: weights } : { surfaces };
21980
+ writeJson(path6, profile);
21981
+ console.log(
21982
+ `
21983
+ wrote ${path6}
21984
+ ${surfaces.length} surface${surfaces.length === 1 ? "" : "s"}: ${surfaces.map((s) => s.id).join(", ")}
21985
+ ${Object.keys(weights).length > 0 ? `weights: ${Object.entries(weights).map(([k, v]) => `${k}=${v}`).join(" ")}` : "weights: 1 across every domain"}
21986
+ `
21987
+ );
21988
+ process.exit(0);
21989
+ }
21990
+ function runScore(args, common) {
21991
+ const { single, flags, positionals } = collect(args);
21992
+ if (flags.has("help")) {
21993
+ console.log(SCORE_USAGE);
21994
+ process.exit(0);
21995
+ }
21996
+ const [criterionId, surfaceArg, levelArg] = positionals;
21997
+ if (criterionId === void 0 || surfaceArg === void 0 || levelArg === void 0) {
21998
+ fail12(`kritik: score takes <criterion> <surface> <level>
21999
+
22000
+ ${SCORE_USAGE}`);
22001
+ }
22002
+ const library = loadLibraryOrFail(common.root);
22003
+ const profile = profileOrFail(common.root);
22004
+ const criterion = criterionOrFail(library, criterionId);
22005
+ const surface = surfaceOrFail(profile, surfaceArg);
22006
+ const level = intOrFail("level", levelArg, 0, 4);
22007
+ const appliesTo = criterion.applies_to;
22008
+ if (Array.isArray(appliesTo) && !appliesTo.includes(surface)) {
22009
+ fail12(
22010
+ `kritik: ${criterionId} does not apply to "${surface}" (applies to: ${appliesTo.join(", ")}).
22011
+ Scoring it there would produce a cell nothing rolls up.`
22012
+ );
22013
+ }
22014
+ if (single.evidence === void 0 || single.evidence.trim() === "") {
22015
+ fail12(`kritik: --evidence is required \u2014 a score without a citation is an opinion, not an assessment.
22016
+
22017
+ ${SCORE_USAGE}`);
22018
+ }
22019
+ const evidence = textOrFile(single.evidence);
22020
+ const auditId = auditForWrite(common.root, single.audit);
22021
+ const file2 = loadScoresOrEmpty(common.root, auditId);
22022
+ const assessment = {
22023
+ criterion_id: criterionId,
22024
+ surface,
22025
+ level,
22026
+ evidence,
22027
+ audit_id: auditId,
22028
+ ...single.commit !== void 0 ? { commit: single.commit } : {},
22029
+ ts: (/* @__PURE__ */ new Date()).toISOString()
22030
+ };
22031
+ const { assessments, replaced } = upsertAssessment(file2.assessments, assessment);
22032
+ saveScores(common.root, auditId, {
22033
+ ...file2,
22034
+ audit_id: auditId,
22035
+ framework_version: file2.framework_version ?? library.version,
22036
+ ...single.commit !== void 0 ? { commit: single.commit } : {},
22037
+ assessments
22038
+ });
22039
+ const anchor = criterion.level_anchors?.[`l${level}`];
22040
+ console.log(
22041
+ `
22042
+ ${replaced ? `re-scored ${criterionId} x ${surface}: ${replaced.level} -> ${level}` : `scored ${criterionId} x ${surface} at ${level}`}${anchor ? ` \u2014 ${anchor}` : ""}
22043
+ ${assessments.length} assessment${assessments.length === 1 ? "" : "s"} in ${auditId} -> ${scoresPath(common.root, auditId)}`
22044
+ );
22045
+ if (level < DEFAULT_TARGET_LEVEL) {
22046
+ console.log(
22047
+ ` below the level-${DEFAULT_TARGET_LEVEL} target \u2014 open a finding, or say in the evidence why this surface should not reach it.`
22048
+ );
22049
+ }
22050
+ console.log("");
22051
+ process.exit(0);
22052
+ }
22053
+ function runFinding(args, common) {
22054
+ const [action, ...rest] = args;
22055
+ if (action === void 0 || action === "--help" || action === "-h") {
22056
+ console.log(FINDING_USAGE);
22057
+ process.exit(action === void 0 ? 1 : 0);
22058
+ }
22059
+ if (action === "open") return runFindingOpen(rest, common);
22060
+ if (action === "resolve") return runFindingResolve(rest, common);
22061
+ if (action === "accept") return runFindingAccept(rest, common);
22062
+ fail12(`kritik: unknown finding action "${action}"
22063
+
22064
+ ${FINDING_USAGE}`);
22065
+ }
22066
+ function runFindingOpen(args, common) {
22067
+ const { single, flags, positionals } = collect(args);
22068
+ if (flags.has("help")) {
22069
+ console.log(FINDING_USAGE);
22070
+ process.exit(0);
22071
+ }
22072
+ const [criterionId, surfaceArg] = positionals;
22073
+ if (criterionId === void 0 || surfaceArg === void 0) {
22074
+ fail12(`kritik: finding open takes <criterion> <surface>
22075
+
22076
+ ${FINDING_USAGE}`);
22077
+ }
22078
+ const library = loadLibraryOrFail(common.root);
22079
+ const profile = profileOrFail(common.root);
22080
+ criterionOrFail(library, criterionId);
22081
+ const surface = surfaceOrFail(profile, surfaceArg, { allowCrossSurface: true });
22082
+ if (single.title === void 0 || single.title.trim() === "") {
22083
+ fail12(`kritik: --title is required \u2014 one line naming the actual defect, not the category.
22084
+
22085
+ ${FINDING_USAGE}`);
22086
+ }
22087
+ if (single.evidence === void 0 || single.evidence.trim() === "") {
22088
+ fail12(`kritik: --evidence is required \u2014 file:line citations someone can check.
22089
+
22090
+ ${FINDING_USAGE}`);
22091
+ }
22092
+ const impact = intOrFail("--impact", single.impact, 1, 5);
22093
+ const likelihood = intOrFail("--likelihood", single.likelihood, 1, 5);
22094
+ const cost = single.cost;
22095
+ if (cost === void 0 || !REMEDIATION_COSTS.includes(cost)) {
22096
+ fail12(
22097
+ `kritik: --cost must be one of ${REMEDIATION_COSTS.join(", ")} \u2014 it decides priority, so a High that is cheap to fix outranks a High that is not.`
22098
+ );
22099
+ }
22100
+ const auditId = auditForWrite(common.root, single.audit);
22101
+ const file2 = loadFindings(common.root, auditId);
22102
+ const taken = new Set(file2.findings.map((f) => f.id));
22103
+ const id = single.id ?? mintFindingId(auditId, criterionId, surface, taken, library);
22104
+ if (single.id !== void 0 && taken.has(single.id)) {
22105
+ fail12(`kritik: finding "${single.id}" already exists in ${auditId} \u2014 resolve or accept it rather than reopening the id.`);
22106
+ }
22107
+ const nodes = single.nodes?.split(",").map((n) => n.trim()).filter((n) => n !== "");
22108
+ const finding = {
22109
+ id,
22110
+ criterion_id: criterionId,
22111
+ surface,
22112
+ title: single.title,
22113
+ detail: single.detail ?? single.title,
22114
+ evidence: textOrFile(single.evidence),
22115
+ impact,
22116
+ likelihood,
22117
+ cost,
22118
+ status: "open",
22119
+ ...single.remediation !== void 0 ? { remediation: single.remediation } : {},
22120
+ ...nodes !== void 0 && nodes.length > 0 ? { node_ids: nodes } : {},
22121
+ ...single["issue-url"] !== void 0 ? { issue_url: single["issue-url"] } : {}
22122
+ };
22123
+ const { findings } = upsertFinding(file2.findings, finding);
22124
+ saveFindings(common.root, auditId, {
22125
+ ...file2,
22126
+ audit_id: auditId,
22127
+ framework_version: file2.framework_version ?? library.version,
22128
+ findings
22129
+ });
22130
+ const severity = severityOf(finding, library);
22131
+ const priority = priorityOf(finding, library);
22132
+ console.log(
22133
+ `
22134
+ opened ${id} \u2014 ${finding.title}
22135
+ ${severity} / ${priority} (impact ${impact} x likelihood ${likelihood} = ${impact * likelihood}, cost ${cost}) on ${surface}
22136
+ ${findings.length} finding${findings.length === 1 ? "" : "s"} in ${auditId}`
22137
+ );
22138
+ reportJournal(common.root, [findingOpenedInput(finding, library)], common);
22139
+ if (priority === "P0") {
22140
+ console.log(` P0: file the issue now \u2014 \`arkaik kritik issue ${criterionId} --surface ${surface} --finding ${id}\``);
22141
+ }
22142
+ console.log("");
22143
+ process.exit(0);
22144
+ }
22145
+ function runFindingResolve(args, common) {
22146
+ const { single, flags, positionals } = collect(args);
22147
+ if (flags.has("help")) {
22148
+ console.log(FINDING_USAGE);
22149
+ process.exit(0);
22150
+ }
22151
+ const id = positionals[0];
22152
+ if (id === void 0) fail12(`kritik: finding resolve takes <finding-id>
22153
+
22154
+ ${FINDING_USAGE}`);
22155
+ const located = locateFinding(common.root, id);
22156
+ if (!located) fail12(`kritik: no finding "${id}" in any audit under ${common.root}/docs/quality/audits/`);
22157
+ if (located.finding.status === "resolved") {
22158
+ console.log(`
22159
+ ${id} is already resolved \u2014 nothing written.
22160
+ `);
22161
+ process.exit(0);
22162
+ }
22163
+ const { findings, finding } = resolveFinding(located.file.findings, id, single.by);
22164
+ saveFindings(common.root, located.auditId, { ...located.file, findings });
22165
+ console.log(`
22166
+ resolved ${id} \u2014 ${located.finding.title}` + (single.by ? `
22167
+ by ${single.by}` : ""));
22168
+ if (single.by === void 0) {
22169
+ console.log(` no --by: without the PR or commit that closed it, "resolved" means "we stopped looking".`);
22170
+ }
22171
+ reportJournal(common.root, [findingResolvedInput(finding ?? located.finding, single.by)], common);
22172
+ console.log("");
22173
+ process.exit(0);
22174
+ }
22175
+ function runFindingAccept(args, common) {
22176
+ const { single, flags, positionals } = collect(args);
22177
+ if (flags.has("help")) {
22178
+ console.log(FINDING_USAGE);
22179
+ process.exit(0);
22180
+ }
22181
+ const id = positionals[0];
22182
+ if (id === void 0) fail12(`kritik: finding accept takes <finding-id>
22183
+
22184
+ ${FINDING_USAGE}`);
22185
+ if (single.note === void 0 || single.note.trim() === "") {
22186
+ fail12(`kritik: --note is required \u2014 an accepted risk is a decision and reads like one.`);
22187
+ }
22188
+ const located = locateFinding(common.root, id);
22189
+ if (!located) fail12(`kritik: no finding "${id}" in any audit under ${common.root}/docs/quality/audits/`);
22190
+ const { findings } = acceptFinding(located.file.findings, id, single.note);
22191
+ saveFindings(common.root, located.auditId, { ...located.file, findings });
22192
+ console.log(
22193
+ `
22194
+ accepted ${id} as a known risk \u2014 ${located.finding.title}
22195
+ ${single.note}
22196
+ no journal event: acceptance is a state of the finding, not something that happened to the product.
22197
+ `
22198
+ );
22199
+ process.exit(0);
22200
+ }
22201
+ function runMatrix(args, common) {
22202
+ const { flags, positionals } = collect(args, [], ["json", "record"]);
22203
+ if (flags.has("help")) {
22204
+ console.log(MATRIX_USAGE);
22205
+ process.exit(0);
22206
+ }
22207
+ const library = loadLibraryOrFail(common.root);
22208
+ let auditId;
22209
+ try {
22210
+ auditId = positionals[0] ?? newestAuditId(common.root);
22211
+ } catch (error51) {
22212
+ return fail12(`kritik: ${error51.message}`);
22213
+ }
22214
+ let computed;
22215
+ try {
22216
+ computed = computeAuditMatrix(common.root, auditId, library);
22217
+ } catch (error51) {
22218
+ return fail12(`kritik: ${error51.message}`);
22219
+ }
22220
+ const { section, matrix, file: file2 } = computed;
22221
+ if (flags.has("json")) {
22222
+ console.log(JSON.stringify(file2, null, 2));
22223
+ } else {
22224
+ const domainNames = new Map((library.domains ?? []).map((d) => [d.code, d.name]));
22225
+ console.log(`
22226
+ ${renderMatrixMarkdown(matrix, domainNames)}
22227
+ `);
22228
+ const open = section.findings.filter(isOpenFinding);
22229
+ const lanes = { P0: 0, P1: 0, P2: 0, P3: 0 };
22230
+ for (const finding of open) lanes[priorityOf(finding, library)]++;
22231
+ const counts = matrix.finding_counts;
22232
+ console.log(
22233
+ `${section.assessments.length} assessments \xB7 ${open.length} open findings (${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low)
22234
+ lanes: P0 ${lanes.P0} \xB7 P1 ${lanes.P1} \xB7 P2 ${lanes.P2} \xB7 P3 ${lanes.P3}`
22235
+ );
22236
+ const p0 = open.filter((finding) => priorityOf(finding, library) === "P0");
22237
+ if (p0.length > 0) {
22238
+ console.log(`
22239
+ P0 \u2014 fix first:`);
22240
+ for (const finding of p0) {
22241
+ console.log(` [${severityOf(finding, library)}] ${finding.surface} \xB7 ${finding.id} \u2014 ${finding.title}`);
22242
+ }
22243
+ }
22244
+ console.log(`
22245
+ wrote ${matrixPath(common.root, auditId)}`);
22246
+ }
22247
+ if (flags.has("record")) {
22248
+ reportJournal(
22249
+ common.root,
22250
+ [auditCompletedInput(matrix, { audit_id: auditId, framework_version: file2.framework_version, ...file2.commit !== void 0 ? { commit: file2.commit } : {} })],
22251
+ common
22252
+ );
22253
+ }
22254
+ console.log("");
22255
+ process.exit(0);
22256
+ }
22257
+ function tripsSinceLastAudit(root, common) {
22258
+ const journal = resolveJournal(root, common.bundlePath);
22259
+ if (!journal.present) return [];
22260
+ const events = orderEvents(readFullJournalEvents(journal.journalPath));
22261
+ const lastAudit = events.map((event) => event.type).lastIndexOf("quality.audit.completed");
22262
+ return events.slice(lastAudit + 1).filter((event) => event.type === "quality.signal.tripped");
22263
+ }
22264
+ function runSignals(args, common) {
22265
+ const { single, flags } = collect(args, [], ["json"]);
22266
+ if (flags.has("help")) {
22267
+ console.log(SIGNALS_USAGE);
22268
+ process.exit(0);
22269
+ }
22270
+ const library = loadLibraryOrFail(common.root);
22271
+ const profile = profileOrFail(common.root);
22272
+ if (single.trip !== void 0) {
22273
+ const criterion = criterionOrFail(library, single.trip);
22274
+ if (single.surface === void 0) fail12(`kritik: --trip needs --surface`);
22275
+ const surface = surfaceOrFail(profile, single.surface);
22276
+ if (single.signal === void 0) fail12(`kritik: --trip needs --signal (a run-sheet index, or the statement itself)`);
22277
+ const signals = criterion.signals ?? [];
22278
+ const index = Number(single.signal);
22279
+ const signal = Number.isInteger(index) && index >= 0 && index < signals.length ? signals[index] : single.signal;
22280
+ console.log(`
22281
+ tripped ${criterion.id} x ${surface}
22282
+ ${signal}`);
22283
+ reportJournal(
22284
+ common.root,
22285
+ [signalTrippedInput({ criterion_id: criterion.id, surface, signal, ...single.detail !== void 0 ? { detail: single.detail } : {} })],
22286
+ common
22287
+ );
22288
+ console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
22289
+ `);
22290
+ process.exit(0);
22291
+ }
22292
+ const filter = {
22293
+ ...single.surface !== void 0 ? { surface: surfaceOrFail(profile, single.surface) } : {},
22294
+ ...single.criterion !== void 0 ? { criterion: criterionOrFail(library, single.criterion).id } : {},
22295
+ ...single.domain !== void 0 ? { domain: single.domain } : {}
22296
+ };
22297
+ const rows = signalRunSheet(library, profile.surfaces ?? [], filter);
22298
+ const trips = tripsSinceLastAudit(common.root, common);
22299
+ if (flags.has("json")) {
22300
+ console.log(JSON.stringify({ signals: rows, tripped_since_last_audit: trips }, null, 2));
22301
+ process.exit(trips.length > 0 ? 1 : 0);
22302
+ }
22303
+ const filtered = Object.keys(filter).length > 0;
22304
+ if (filtered) {
22305
+ console.log("");
22306
+ let current = "";
22307
+ for (const row of rows) {
22308
+ const key = `${row.criterion_id} x ${row.surface}`;
22309
+ if (key !== current) {
22310
+ current = key;
22311
+ console.log(` ${key}`);
22312
+ }
22313
+ console.log(` [${row.index}] ${row.signal}`);
22314
+ }
22315
+ console.log(`
22316
+ ${rows.length} check${rows.length === 1 ? "" : "s"}.`);
22317
+ } else {
22318
+ const criteria = new Set(rows.map((row) => row.criterion_id)).size;
22319
+ console.log(
22320
+ `
22321
+ ${rows.length} checks across ${criteria} criteria and ${(profile.surfaces ?? []).length} surfaces.
22322
+ Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.` + (listAuditIds(common.root).length > 1 ? `
22323
+ Comparing two audits is \`arkaik kritik regressions\`.` : "")
22324
+ );
22325
+ }
22326
+ if (trips.length > 0) {
22327
+ console.log(`
22328
+ tripped since the last recorded audit:`);
22329
+ for (const trip of trips) {
22330
+ const event = trip;
22331
+ console.log(` ${event.criterion_id} x ${event.surface} \u2014 ${event.signal}`);
22332
+ }
22333
+ console.log("");
22334
+ process.exit(1);
22335
+ }
22336
+ console.log("");
22337
+ process.exit(0);
22338
+ }
22339
+ function auditPair(root, from, to) {
22340
+ const audits = listAuditIds(root);
22341
+ if (audits.length < 2) {
22342
+ fail12(
22343
+ `kritik: regressions needs two audits to compare \u2014 ${audits.length === 0 ? "docs/quality/audits/ holds none" : `only "${audits[0]}" exists`}.
22344
+ A regression is the difference between two readings; one reading is a baseline.`
22345
+ );
22346
+ }
22347
+ const known = (id) => {
22348
+ if (!audits.includes(id)) fail12(`kritik: no audit "${id}" under docs/quality/audits/ (have: ${audits.join(", ")})`);
22349
+ return id;
22350
+ };
22351
+ const newer = to === void 0 ? audits[audits.length - 1] : known(to);
22352
+ const older = from === void 0 ? audits[audits.indexOf(newer) - 1] : known(from);
22353
+ if (older === void 0) {
22354
+ fail12(`kritik: "${newer}" is the oldest audit \u2014 there is nothing before it to compare against.`);
22355
+ }
22356
+ if (older === newer) {
22357
+ fail12(`kritik: --from and --to name the same audit ("${newer}") \u2014 a regression needs two readings.`);
22358
+ }
22359
+ if (audits.indexOf(older) > audits.indexOf(newer)) {
22360
+ fail12(`kritik: --from "${older}" is newer than --to "${newer}" \u2014 swap them, or the comparison inverts.`);
22361
+ }
22362
+ return { from: older, to: newer };
22363
+ }
22364
+ function runRegressions(args, common) {
22365
+ const { single, flags } = collect(args, [], ["json", "record"]);
22366
+ if (flags.has("help")) {
22367
+ console.log(REGRESSIONS_USAGE);
22368
+ process.exit(0);
22369
+ }
22370
+ const library = loadLibraryOrFail(common.root);
22371
+ profileOrFail(common.root);
22372
+ const { from, to } = auditPair(common.root, single.from, single.to);
22373
+ let regressions;
22374
+ try {
22375
+ regressions = detectRegressions(
22376
+ loadQualitySection(common.root, from, library),
22377
+ loadQualitySection(common.root, to, library),
22378
+ library
22379
+ );
22380
+ } catch (error51) {
22381
+ return fail12(`kritik: ${error51.message}`);
22382
+ }
22383
+ if (flags.has("json")) {
22384
+ console.log(JSON.stringify({ from, to, total: regressions.length, regressions }, null, 2));
22385
+ } else if (regressions.length === 0) {
22386
+ console.log(`
22387
+ nothing regressed between ${from} and ${to}.
22388
+ `);
22389
+ } else {
22390
+ console.log("");
22391
+ for (const regression of regressions) {
22392
+ console.log(` [${regression.kind}] ${regression.criterion_id} x ${regression.surface}`);
22393
+ console.log(` ${regression.detail}`);
22394
+ }
22395
+ console.log(`
22396
+ ${regressions.length} regression${regressions.length === 1 ? "" : "s"} between ${from} and ${to}.`);
22397
+ }
22398
+ if (flags.has("record") && regressions.length > 0) {
22399
+ reportJournal(
22400
+ common.root,
22401
+ regressions.map(
22402
+ (regression) => signalTrippedInput({
22403
+ criterion_id: regression.criterion_id,
22404
+ surface: regression.surface,
22405
+ signal: regression.signal,
22406
+ detail: regression.detail
22407
+ })
22408
+ ),
22409
+ common
22410
+ );
22411
+ console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
22412
+ `);
22413
+ }
22414
+ process.exit(regressions.length > 0 ? 1 : 0);
22415
+ }
22416
+ function runIssue(args, common) {
22417
+ const { single, flags, positionals } = collect(args);
22418
+ if (flags.has("help")) {
22419
+ console.log(ISSUE_USAGE);
22420
+ process.exit(0);
22421
+ }
22422
+ const criterionId = positionals[0];
22423
+ if (criterionId === void 0) fail12(`kritik: issue takes <criterion>
22424
+
22425
+ ${ISSUE_USAGE}`);
22426
+ if (single.surface === void 0) fail12(`kritik: --surface is required
22427
+
22428
+ ${ISSUE_USAGE}`);
22429
+ const library = loadLibraryOrFail(common.root);
22430
+ const criterion = criterionOrFail(library, criterionId);
22431
+ const declared = loadProfile(common.root);
22432
+ if (declared) surfaceOrFail(declared, single.surface, { allowCrossSurface: true });
22433
+ let finding;
22434
+ if (single.finding !== void 0) {
22435
+ const located = locateFinding(common.root, single.finding);
22436
+ if (!located) fail12(`kritik: no finding "${single.finding}" in any audit`);
22437
+ finding = located.finding;
22438
+ }
22439
+ const rendered = renderIssue(criterion, {
22440
+ surface: single.surface,
22441
+ ...single.level !== void 0 ? { level: single.level } : {},
22442
+ ...finding !== void 0 ? { finding, library } : {}
22443
+ });
22444
+ process.stdout.write(`Title: ${rendered.title}
22445
+ `);
22446
+ process.stdout.write(`Labels: ${rendered.labels.join(", ")}
22447
+
22448
+ `);
22449
+ process.stdout.write(`${rendered.body}
22450
+ `);
22451
+ if (rendered.remediation) process.stdout.write(`
22452
+ <!-- Typical remediation: ${rendered.remediation} -->
22453
+ `);
22454
+ process.exit(0);
22455
+ }
22456
+ function runCriterion(args, common) {
22457
+ const [action, ...rest] = args;
22458
+ if (action === void 0 || action === "--help" || action === "-h") {
22459
+ console.log(CRITERION_USAGE);
22460
+ process.exit(action === void 0 ? 1 : 0);
22461
+ }
22462
+ if (action !== "add") fail12(`kritik: unknown criterion action "${action}" (only \`add\`)
22463
+
22464
+ ${CRITERION_USAGE}`);
22465
+ const { single, many, flags } = collect(rest, ["anchor", "signal", "check", "label"], ["template", "force"]);
22466
+ if (flags.has("help")) {
22467
+ console.log(CRITERION_USAGE);
22468
+ process.exit(0);
22469
+ }
22470
+ const { library: pack } = (() => {
22471
+ try {
22472
+ const loaded = loadKritikLibrary(common.root);
22473
+ return { library: loaded.pack.library };
22474
+ } catch (error51) {
22475
+ return fail12(`kritik: ${error51.message}`);
22476
+ }
22477
+ })();
22478
+ if (flags.has("template")) {
22479
+ console.log(JSON.stringify(CRITERION_TEMPLATE, null, 2));
22480
+ process.exit(0);
22481
+ }
22482
+ let criterion;
22483
+ try {
22484
+ if (single.from !== void 0) {
22485
+ if (!existsSync16(single.from)) fail12(`kritik: no file at ${single.from}`);
22486
+ criterion = JSON.parse(readFileSync15(single.from, "utf8"));
22487
+ } else {
22488
+ const anchors = {};
22489
+ for (const spec of many.anchor) {
22490
+ const at2 = spec.indexOf("=");
22491
+ if (at2 <= 0) fail12(`kritik: --anchor wants lN=<text> (got "${spec}")`);
22492
+ anchors[spec.slice(0, at2).trim()] = spec.slice(at2 + 1);
22493
+ }
22494
+ const draft = {
22495
+ id: single.id ?? "",
22496
+ domain: single.domain ?? "",
22497
+ ...single.subcategory !== void 0 ? { subcategory: single.subcategory } : {},
22498
+ name: single.name ?? "",
22499
+ question: single.question ?? "",
22500
+ ...single.definition !== void 0 ? { definition: single.definition } : {},
22501
+ ...single.rationale !== void 0 ? { rationale: single.rationale } : {},
22502
+ appliesTo: (single["applies-to"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
22503
+ anchors,
22504
+ ...single.weight !== void 0 ? { weight: Number(single.weight) } : {},
22505
+ ...single.impact !== void 0 ? { impact: Number(single.impact) } : {},
22506
+ signals: many.signal,
22507
+ checklist: many.check,
22508
+ ...single.remediation !== void 0 ? { remediation: single.remediation } : {},
22509
+ labels: many.label
22510
+ };
22511
+ criterion = buildCriterion(draft);
22512
+ }
22513
+ } catch (error51) {
22514
+ return fail12(`kritik: ${error51.message}
22515
+
22516
+ ${CRITERION_USAGE}`);
22517
+ }
22518
+ let written;
22519
+ try {
22520
+ written = addCriterionToOverlay(common.root, pack, criterion, {
22521
+ force: flags.has("force"),
22522
+ ...single["domain-name"] !== void 0 ? { domainName: single["domain-name"] } : {}
22523
+ });
22524
+ } catch (error51) {
22525
+ return fail12(`kritik: ${error51.message}`);
22526
+ }
22527
+ console.log(
22528
+ `
22529
+ ${written.replaced ? "replaced" : "added"} ${criterion.id} (${criterion.domain}) -> ${written.path}
22530
+ applies to ${(criterion.applies_to ?? []).join(", ") || "every surface"}` + (written.addedDomain !== void 0 ? `
22531
+ declared a new domain: ${written.addedDomain}` : "") + `
22532
+ it scores and rolls up exactly like a pack criterion \u2014 \`arkaik kritik issue ${criterion.id} --surface <s>\`
22533
+ `
22534
+ );
22535
+ process.exit(0);
22536
+ }
22537
+ function runKritik(args) {
22538
+ const { rest, common } = takeCommon(args);
22539
+ const [sub, ...subArgs] = rest;
22540
+ if (sub === void 0 || sub === "--help" || sub === "-h" || sub === "help") {
22541
+ console.log(USAGE13);
22542
+ process.exit(sub === void 0 ? 1 : 0);
22543
+ }
22544
+ switch (sub) {
22545
+ case "profile":
22546
+ return runProfile(subArgs, common);
22547
+ case "score":
22548
+ return runScore(subArgs, common);
22549
+ case "finding":
22550
+ return runFinding(subArgs, common);
22551
+ case "matrix":
22552
+ return runMatrix(subArgs, common);
22553
+ case "signals":
22554
+ return runSignals(subArgs, common);
22555
+ case "regressions":
22556
+ return runRegressions(subArgs, common);
22557
+ case "issue":
22558
+ return runIssue(subArgs, common);
22559
+ case "criterion":
22560
+ return runCriterion(subArgs, common);
22561
+ default:
22562
+ fail12(`kritik: unknown subcommand "${sub}"
22563
+
22564
+ ${USAGE13}`);
22565
+ }
22566
+ }
22567
+
20026
22568
  // src/index.ts
20027
- var USAGE13 = `arkaik \u2014 CLI for Arkaik project bundles
22569
+ var USAGE14 = `arkaik \u2014 CLI for Arkaik project bundles
20028
22570
 
20029
22571
  Usage:
20030
22572
  arkaik <command> [options]
@@ -20036,25 +22578,26 @@ Commands:
20036
22578
  release <version> [path] Tag a release (append release.tagged) and draft its notes.
20037
22579
  deliverable <title> [path] Record a deliverable (append deliverable.shipped).
20038
22580
  sync [options] [path] Mirror external ref status (GitHub issues/PRs) into node refs.
20039
- pack [options] [path] Produce a single self-contained interchange bundle (embeds the journal).
22581
+ pack [options] [path] Produce a self-contained interchange bundle (embeds journal + quality).
20040
22582
  open [options] [path] Validate, then hand off the packed bundle to arkaik.app import.
20041
- push [options] [path] Validate, pack (journal stripped), and publish to Publik.
22583
+ push [options] [path] Validate, pack (journal + quality stripped), and publish to Publik.
20042
22584
  --delete <id> --key <owner_key> removes a snapshot.
20043
22585
  link [options] [path] Point this repo at a hosted project so an agent can edit it.
20044
22586
  --list shows the projects your token can reach.
20045
- restore [options] [path] Replace the linked hosted project's bundle + journal (backs up first).
22587
+ restore [options] [path] Replace the linked project's bundle, journal + quality (backs up first).
20046
22588
  bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
22589
+ kritik <sub> [options] Quality audits: score criteria, open findings, roll up the matrix.
20047
22590
 
20048
22591
  Options:
20049
22592
  -h, --help Show this help.
20050
22593
  -v, --version Print the version.
20051
22594
 
20052
22595
  Run "arkaik <command> --help" for command-specific help.`;
20053
- var VERSION = "0.1.1";
22596
+ var VERSION = "0.3.0";
20054
22597
  function main(argv) {
20055
22598
  const [command, ...rest] = argv;
20056
22599
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
20057
- console.log(USAGE13);
22600
+ console.log(USAGE14);
20058
22601
  process.exit(0);
20059
22602
  }
20060
22603
  if (command === "--version" || command === "-v" || command === "version") {
@@ -20098,10 +22641,13 @@ function main(argv) {
20098
22641
  case "bootstrap":
20099
22642
  runBootstrap(rest);
20100
22643
  return;
22644
+ case "kritik":
22645
+ runKritik(rest);
22646
+ return;
20101
22647
  default:
20102
22648
  console.error(`Unknown command: ${command}
20103
22649
  `);
20104
- console.error(USAGE13);
22650
+ console.error(USAGE14);
20105
22651
  process.exit(1);
20106
22652
  }
20107
22653
  }