arkaik 0.2.0 → 0.4.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/assets/skill/references/schema.md +1 -0
- package/dist/assets/skill/scripts/validate-bundle.js +2 -2
- package/dist/index.js +786 -291
- package/dist/io.js +39 -4
- package/package.json +1 -1
- package/src/io.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -15216,6 +15216,13 @@ var QualityFindingResolvedEventSchema = external_exports.object({
|
|
|
15216
15216
|
resolved_by: external_exports.string().optional(),
|
|
15217
15217
|
node_ids: external_exports.array(external_exports.string()).optional()
|
|
15218
15218
|
}).catchall(external_exports.unknown());
|
|
15219
|
+
var QualityFindingAcceptedEventSchema = external_exports.object({
|
|
15220
|
+
...envelope,
|
|
15221
|
+
type: external_exports.literal("quality.finding.accepted"),
|
|
15222
|
+
finding_id: external_exports.string(),
|
|
15223
|
+
reason: external_exports.string(),
|
|
15224
|
+
node_ids: external_exports.array(external_exports.string()).optional()
|
|
15225
|
+
}).catchall(external_exports.unknown());
|
|
15219
15226
|
var QualitySignalTrippedEventSchema = external_exports.object({
|
|
15220
15227
|
...envelope,
|
|
15221
15228
|
type: external_exports.literal("quality.signal.tripped"),
|
|
@@ -15243,6 +15250,7 @@ var JOURNAL_EVENT_SCHEMAS = {
|
|
|
15243
15250
|
"quality.audit.completed": QualityAuditCompletedEventSchema,
|
|
15244
15251
|
"quality.finding.opened": QualityFindingOpenedEventSchema,
|
|
15245
15252
|
"quality.finding.resolved": QualityFindingResolvedEventSchema,
|
|
15253
|
+
"quality.finding.accepted": QualityFindingAcceptedEventSchema,
|
|
15246
15254
|
"quality.signal.tripped": QualitySignalTrippedEventSchema
|
|
15247
15255
|
};
|
|
15248
15256
|
var KnownJournalEventSchema = external_exports.union([
|
|
@@ -15264,6 +15272,7 @@ var KnownJournalEventSchema = external_exports.union([
|
|
|
15264
15272
|
QualityAuditCompletedEventSchema,
|
|
15265
15273
|
QualityFindingOpenedEventSchema,
|
|
15266
15274
|
QualityFindingResolvedEventSchema,
|
|
15275
|
+
QualityFindingAcceptedEventSchema,
|
|
15267
15276
|
QualitySignalTrippedEventSchema
|
|
15268
15277
|
]);
|
|
15269
15278
|
|
|
@@ -15351,6 +15360,9 @@ var QualityFindingSchema = external_exports.object({
|
|
|
15351
15360
|
remediation: external_exports.string().optional(),
|
|
15352
15361
|
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
15362
|
issue_url: external_exports.string().optional(),
|
|
15363
|
+
commit: external_exports.string().optional().meta({
|
|
15364
|
+
description: "Commit SHA anchoring the file:line evidence. Required by policy on any finding written away from a checkout (issue #400 decision 4) \u2014 without it a stale citation is indistinguishable from a wrong one."
|
|
15365
|
+
}),
|
|
15354
15366
|
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
15367
|
}).catchall(external_exports.unknown()).meta({
|
|
15356
15368
|
id: "QualityFinding",
|
|
@@ -16611,6 +16623,15 @@ function validateBundle(input) {
|
|
|
16611
16623
|
);
|
|
16612
16624
|
}
|
|
16613
16625
|
}
|
|
16626
|
+
if (type === "quality.finding.accepted" && typeof event.finding_id === "string") {
|
|
16627
|
+
if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
|
|
16628
|
+
warn(
|
|
16629
|
+
`journal[${index}].finding_id`,
|
|
16630
|
+
"quality-accepted-never-opened",
|
|
16631
|
+
`Journal event ${index}: accepts finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
|
|
16632
|
+
);
|
|
16633
|
+
}
|
|
16634
|
+
}
|
|
16614
16635
|
});
|
|
16615
16636
|
}
|
|
16616
16637
|
for (const finding of crossCheckJournal(bundle)) findings.push(finding);
|
|
@@ -17074,11 +17095,14 @@ function resolveFinding(findings, id, resolvedBy) {
|
|
|
17074
17095
|
...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {}
|
|
17075
17096
|
});
|
|
17076
17097
|
}
|
|
17077
|
-
function
|
|
17078
|
-
|
|
17079
|
-
const detail = existing === void 0 ? note : `${existing.detail}
|
|
17098
|
+
function acceptedDetail(detail, note) {
|
|
17099
|
+
return `${detail}
|
|
17080
17100
|
|
|
17081
17101
|
Accepted risk: ${note}`.trim();
|
|
17102
|
+
}
|
|
17103
|
+
function acceptFinding(findings, id, note) {
|
|
17104
|
+
const existing = findings.find((candidate) => candidate.id === id);
|
|
17105
|
+
const detail = existing === void 0 ? note : acceptedDetail(existing.detail, note);
|
|
17082
17106
|
return patchFinding(findings, id, { status: "accepted-risk", detail });
|
|
17083
17107
|
}
|
|
17084
17108
|
function fillTemplate(text, values) {
|
|
@@ -17210,6 +17234,83 @@ function signalTrippedInput(trip) {
|
|
|
17210
17234
|
};
|
|
17211
17235
|
}
|
|
17212
17236
|
|
|
17237
|
+
// ../schema/src/quality-regressions.ts
|
|
17238
|
+
var REGRESSION_SIGNALS = {
|
|
17239
|
+
"level-drop": "maturity on this cell does not regress",
|
|
17240
|
+
"new-severe-finding": "no open Critical or High finding on this cell",
|
|
17241
|
+
"reopened-finding": "a resolved finding stays resolved"
|
|
17242
|
+
};
|
|
17243
|
+
var cellKey = (criterionId, surface) => `${criterionId}::${surface}`;
|
|
17244
|
+
var rowsOf = (value) => Array.isArray(value) ? value : [];
|
|
17245
|
+
function assessmentsByCell(state) {
|
|
17246
|
+
const cells = /* @__PURE__ */ new Map();
|
|
17247
|
+
for (const assessment of rowsOf(state?.assessments)) {
|
|
17248
|
+
if (typeof assessment?.criterion_id !== "string" || typeof assessment?.surface !== "string") continue;
|
|
17249
|
+
cells.set(cellKey(assessment.criterion_id, assessment.surface), assessment);
|
|
17250
|
+
}
|
|
17251
|
+
return cells;
|
|
17252
|
+
}
|
|
17253
|
+
function severeByCell(state, library) {
|
|
17254
|
+
const cells = /* @__PURE__ */ new Map();
|
|
17255
|
+
for (const finding of rowsOf(state?.findings)) {
|
|
17256
|
+
if (typeof finding?.criterion_id !== "string" || typeof finding?.surface !== "string") continue;
|
|
17257
|
+
if (!isOpenFinding(finding)) continue;
|
|
17258
|
+
const severity = severityOf(finding, library);
|
|
17259
|
+
if (severity !== "critical" && severity !== "high") continue;
|
|
17260
|
+
const key = cellKey(finding.criterion_id, finding.surface);
|
|
17261
|
+
const existing = cells.get(key);
|
|
17262
|
+
if (existing === void 0) cells.set(key, [finding]);
|
|
17263
|
+
else existing.push(finding);
|
|
17264
|
+
}
|
|
17265
|
+
return cells;
|
|
17266
|
+
}
|
|
17267
|
+
function detectRegressions(previous, next, library) {
|
|
17268
|
+
const before = assessmentsByCell(previous);
|
|
17269
|
+
const after = assessmentsByCell(next);
|
|
17270
|
+
const comparable = (key) => before.has(key) && after.has(key);
|
|
17271
|
+
const regressions = [];
|
|
17272
|
+
for (const [key, current] of after) {
|
|
17273
|
+
const earlier = before.get(key);
|
|
17274
|
+
if (earlier === void 0) continue;
|
|
17275
|
+
if (!(Number(current.level) < Number(earlier.level))) continue;
|
|
17276
|
+
regressions.push({
|
|
17277
|
+
kind: "level-drop",
|
|
17278
|
+
criterion_id: current.criterion_id,
|
|
17279
|
+
surface: current.surface,
|
|
17280
|
+
signal: REGRESSION_SIGNALS["level-drop"],
|
|
17281
|
+
detail: `level ${earlier.level} \u2192 ${current.level} (${earlier.audit_id} \u2192 ${current.audit_id})`
|
|
17282
|
+
});
|
|
17283
|
+
}
|
|
17284
|
+
const severeBefore = severeByCell(previous, library);
|
|
17285
|
+
for (const [key, findings] of severeByCell(next, library)) {
|
|
17286
|
+
if (!comparable(key)) continue;
|
|
17287
|
+
if ((severeBefore.get(key) ?? []).length > 0) continue;
|
|
17288
|
+
for (const finding of findings) {
|
|
17289
|
+
regressions.push({
|
|
17290
|
+
kind: "new-severe-finding",
|
|
17291
|
+
criterion_id: finding.criterion_id,
|
|
17292
|
+
surface: finding.surface,
|
|
17293
|
+
signal: REGRESSION_SIGNALS["new-severe-finding"],
|
|
17294
|
+
detail: `${finding.id} \u2014 ${severityOf(finding, library)} (impact ${finding.impact} \xD7 likelihood ${finding.likelihood})`
|
|
17295
|
+
});
|
|
17296
|
+
}
|
|
17297
|
+
}
|
|
17298
|
+
const resolvedBefore = new Set(
|
|
17299
|
+
rowsOf(previous?.findings).filter((finding) => finding?.status === "resolved").map((finding) => finding.id)
|
|
17300
|
+
);
|
|
17301
|
+
for (const finding of rowsOf(next?.findings)) {
|
|
17302
|
+
if (!isOpenFinding(finding) || !resolvedBefore.has(finding.id)) continue;
|
|
17303
|
+
regressions.push({
|
|
17304
|
+
kind: "reopened-finding",
|
|
17305
|
+
criterion_id: finding.criterion_id,
|
|
17306
|
+
surface: finding.surface,
|
|
17307
|
+
signal: REGRESSION_SIGNALS["reopened-finding"],
|
|
17308
|
+
detail: `${finding.id} \u2014 "${finding.title}"`
|
|
17309
|
+
});
|
|
17310
|
+
}
|
|
17311
|
+
return regressions;
|
|
17312
|
+
}
|
|
17313
|
+
|
|
17213
17314
|
// src/commands/init.ts
|
|
17214
17315
|
var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
|
|
17215
17316
|
var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
|
|
@@ -18576,18 +18677,290 @@ ${USAGE6}`);
|
|
|
18576
18677
|
}
|
|
18577
18678
|
|
|
18578
18679
|
// src/commands/pack.ts
|
|
18680
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
18681
|
+
import { dirname as dirname5, extname, resolve as resolve5 } from "node:path";
|
|
18682
|
+
|
|
18683
|
+
// src/lib/kritik-io.ts
|
|
18684
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
18685
|
+
import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
|
|
18686
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18687
|
+
|
|
18688
|
+
// ../schema/src/cli/kritik-audit.ts
|
|
18689
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
18690
|
+
import { join as join4 } from "node:path";
|
|
18691
|
+
|
|
18692
|
+
// ../schema/src/cli/kritik-paths.ts
|
|
18579
18693
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
18580
|
-
import { dirname as dirname3,
|
|
18581
|
-
var
|
|
18582
|
-
var
|
|
18694
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
|
|
18695
|
+
var QUALITY_DIR = "docs/quality";
|
|
18696
|
+
var PROFILE_FILE = "profile.json";
|
|
18697
|
+
var OVERLAY_FILE = "criteria.custom.json";
|
|
18698
|
+
var AUDITS_DIR = "audits";
|
|
18699
|
+
var PACK_FILE = "library.json";
|
|
18700
|
+
function readJson(path6) {
|
|
18701
|
+
const text = readFileSync6(path6, "utf8");
|
|
18702
|
+
try {
|
|
18703
|
+
return JSON.parse(text);
|
|
18704
|
+
} catch (e) {
|
|
18705
|
+
throw new Error(`${path6}: not valid JSON \u2014 ${e.message}`);
|
|
18706
|
+
}
|
|
18707
|
+
}
|
|
18708
|
+
function writeJson(path6, value) {
|
|
18709
|
+
mkdirSync3(dirname3(path6), { recursive: true });
|
|
18710
|
+
writeFileSync5(path6, JSON.stringify(value, null, 2) + "\n");
|
|
18711
|
+
}
|
|
18712
|
+
var profilePath = (root) => join3(root, QUALITY_DIR, PROFILE_FILE);
|
|
18713
|
+
var overlayPath = (root) => join3(root, QUALITY_DIR, OVERLAY_FILE);
|
|
18714
|
+
var auditsDir = (root) => join3(root, QUALITY_DIR, AUDITS_DIR);
|
|
18715
|
+
var auditDir = (root, auditId) => join3(auditsDir(root), auditId);
|
|
18716
|
+
function loadProfile(root) {
|
|
18717
|
+
const path6 = profilePath(root);
|
|
18718
|
+
return existsSync5(path6) ? readJson(path6) : null;
|
|
18719
|
+
}
|
|
18720
|
+
function loadOverlay(root) {
|
|
18721
|
+
const path6 = overlayPath(root);
|
|
18722
|
+
return existsSync5(path6) ? readJson(path6) : null;
|
|
18723
|
+
}
|
|
18724
|
+
|
|
18725
|
+
// ../schema/src/cli/kritik-audit.ts
|
|
18726
|
+
var SCORES_FILE = "scores.json";
|
|
18727
|
+
var FINDINGS_FILE = "findings.json";
|
|
18728
|
+
var MATRIX_FILE = "matrix.json";
|
|
18729
|
+
var scoresPath = (root, auditId) => join4(auditDir(root, auditId), SCORES_FILE);
|
|
18730
|
+
var findingsPath = (root, auditId) => join4(auditDir(root, auditId), FINDINGS_FILE);
|
|
18731
|
+
var matrixPath = (root, auditId) => join4(auditDir(root, auditId), MATRIX_FILE);
|
|
18732
|
+
function listAuditIds(root) {
|
|
18733
|
+
const dir = auditsDir(root);
|
|
18734
|
+
if (!existsSync6(dir)) return [];
|
|
18735
|
+
return readdirSync2(dir).filter((name) => statSync(join4(dir, name)).isDirectory()).sort();
|
|
18736
|
+
}
|
|
18737
|
+
function newestAuditId(root) {
|
|
18738
|
+
const dir = auditsDir(root);
|
|
18739
|
+
if (!existsSync6(dir)) throw new Error(`no audits directory at ${dir}`);
|
|
18740
|
+
const ids = listAuditIds(root);
|
|
18741
|
+
if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
|
|
18742
|
+
return ids[ids.length - 1];
|
|
18743
|
+
}
|
|
18744
|
+
function loadScores(root, auditId) {
|
|
18745
|
+
const path6 = scoresPath(root, auditId);
|
|
18746
|
+
if (!existsSync6(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
|
|
18747
|
+
const file2 = readJson(path6);
|
|
18748
|
+
return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
|
|
18749
|
+
}
|
|
18750
|
+
function loadScoresOrEmpty(root, auditId) {
|
|
18751
|
+
return existsSync6(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
|
|
18752
|
+
}
|
|
18753
|
+
function loadFindings(root, auditId) {
|
|
18754
|
+
const path6 = findingsPath(root, auditId);
|
|
18755
|
+
if (!existsSync6(path6)) return { audit_id: auditId, findings: [] };
|
|
18756
|
+
const file2 = readJson(path6);
|
|
18757
|
+
return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
|
|
18758
|
+
}
|
|
18759
|
+
function saveScores(root, auditId, file2) {
|
|
18760
|
+
writeJson(scoresPath(root, auditId), file2);
|
|
18761
|
+
}
|
|
18762
|
+
function saveFindings(root, auditId, file2) {
|
|
18763
|
+
writeJson(findingsPath(root, auditId), file2);
|
|
18764
|
+
}
|
|
18765
|
+
function requireProfile(root) {
|
|
18766
|
+
const profile = loadProfile(root);
|
|
18767
|
+
if (!profile) {
|
|
18768
|
+
throw new Error(
|
|
18769
|
+
`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).`
|
|
18770
|
+
);
|
|
18771
|
+
}
|
|
18772
|
+
return profile;
|
|
18773
|
+
}
|
|
18774
|
+
function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
|
|
18775
|
+
const findings = loadFindings(root, auditId);
|
|
18776
|
+
return {
|
|
18777
|
+
framework_version: scores.framework_version ?? library.version,
|
|
18778
|
+
profile: requireProfile(root),
|
|
18779
|
+
assessments: scores.assessments,
|
|
18780
|
+
findings: findings.findings
|
|
18781
|
+
};
|
|
18782
|
+
}
|
|
18783
|
+
function stripDerived(finding) {
|
|
18784
|
+
const { severity: _severity, priority: _priority, ...rest } = finding;
|
|
18785
|
+
void _severity;
|
|
18786
|
+
void _priority;
|
|
18787
|
+
return rest;
|
|
18788
|
+
}
|
|
18789
|
+
function requireAudit(root, auditId) {
|
|
18790
|
+
if (!listAuditIds(root).includes(auditId)) {
|
|
18791
|
+
throw new Error(`no audit "${auditId}" under ${auditsDir(root)}`);
|
|
18792
|
+
}
|
|
18793
|
+
}
|
|
18794
|
+
function loadCurrentQualitySection(root, library) {
|
|
18795
|
+
const auditIds = listAuditIds(root);
|
|
18796
|
+
if (auditIds.length === 0) return void 0;
|
|
18797
|
+
const profile = loadProfile(root);
|
|
18798
|
+
if (!profile) return void 0;
|
|
18799
|
+
const cells = /* @__PURE__ */ new Map();
|
|
18800
|
+
const findings = [];
|
|
18801
|
+
let frameworkVersion;
|
|
18802
|
+
for (const id of auditIds) {
|
|
18803
|
+
const scores = loadScoresOrEmpty(root, id);
|
|
18804
|
+
if (typeof scores.framework_version === "string") frameworkVersion = scores.framework_version;
|
|
18805
|
+
for (const assessment of scores.assessments) {
|
|
18806
|
+
cells.set(`${assessment.criterion_id}\0${assessment.surface}`, assessment);
|
|
18807
|
+
}
|
|
18808
|
+
for (const finding of loadFindings(root, id).findings) findings.push(stripDerived(finding));
|
|
18809
|
+
}
|
|
18810
|
+
return {
|
|
18811
|
+
framework_version: frameworkVersion ?? library.version,
|
|
18812
|
+
library,
|
|
18813
|
+
profile,
|
|
18814
|
+
assessments: [...cells.values()],
|
|
18815
|
+
findings
|
|
18816
|
+
};
|
|
18817
|
+
}
|
|
18818
|
+
function loadAuditQualitySection(root, auditId, library) {
|
|
18819
|
+
requireAudit(root, auditId);
|
|
18820
|
+
const section = loadQualitySection(root, auditId, library, loadScoresOrEmpty(root, auditId));
|
|
18821
|
+
return { ...section, library, findings: section.findings.map(stripDerived) };
|
|
18822
|
+
}
|
|
18823
|
+
function computeAuditMatrix(root, auditId, library) {
|
|
18824
|
+
const scores = loadScores(root, auditId);
|
|
18825
|
+
const section = loadQualitySection(root, auditId, library, scores);
|
|
18826
|
+
const matrix = deriveQualityMatrix({ quality: section }, library);
|
|
18827
|
+
const file2 = {
|
|
18828
|
+
audit_id: auditId,
|
|
18829
|
+
commit: scores.commit,
|
|
18830
|
+
framework_version: section.framework_version,
|
|
18831
|
+
matrix: matrix.matrix,
|
|
18832
|
+
overall: matrix.overall,
|
|
18833
|
+
finding_counts: matrix.finding_counts
|
|
18834
|
+
};
|
|
18835
|
+
writeJson(matrixPath(root, auditId), file2);
|
|
18836
|
+
return { section, matrix, file: file2 };
|
|
18837
|
+
}
|
|
18838
|
+
function renderMatrixMarkdown(matrix, domainNames) {
|
|
18839
|
+
const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
|
|
18840
|
+
const lines = [];
|
|
18841
|
+
lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
|
|
18842
|
+
lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
|
|
18843
|
+
for (const domain2 of matrix.domains) {
|
|
18844
|
+
const label = domainNames.get(domain2) ?? domain2;
|
|
18845
|
+
lines.push(
|
|
18846
|
+
`| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
|
|
18847
|
+
);
|
|
18848
|
+
}
|
|
18849
|
+
lines.push(
|
|
18850
|
+
`| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
|
|
18851
|
+
const score = matrix.overall[s];
|
|
18852
|
+
return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
|
|
18853
|
+
}).join(" | ")} |`
|
|
18854
|
+
);
|
|
18855
|
+
return lines.join("\n");
|
|
18856
|
+
}
|
|
18857
|
+
function locateFinding(root, id) {
|
|
18858
|
+
for (const auditId of [...listAuditIds(root)].reverse()) {
|
|
18859
|
+
const file2 = loadFindings(root, auditId);
|
|
18860
|
+
const finding = file2.findings.find((candidate) => candidate.id === id);
|
|
18861
|
+
if (finding) return { auditId, file: file2, finding };
|
|
18862
|
+
}
|
|
18863
|
+
return void 0;
|
|
18864
|
+
}
|
|
18865
|
+
|
|
18866
|
+
// src/lib/kritik-io.ts
|
|
18867
|
+
var KRITIK_ACTOR = "arkaik-cli";
|
|
18868
|
+
var DEFAULT_BUNDLE_PATH5 = join5("docs", "arkaik", "bundle.json");
|
|
18869
|
+
var VENDORED_PACK = join5(QUALITY_DIR, PACK_FILE);
|
|
18870
|
+
var BUNDLED_PACK = join5(dirname4(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
|
|
18871
|
+
function resolvePack(root) {
|
|
18872
|
+
const vendored = join5(root, VENDORED_PACK);
|
|
18873
|
+
if (existsSync7(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
|
|
18874
|
+
if (!existsSync7(BUNDLED_PACK)) {
|
|
18875
|
+
throw new Error(
|
|
18876
|
+
`no criteria pack found. Looked in:
|
|
18877
|
+
${vendored}
|
|
18878
|
+
${BUNDLED_PACK}
|
|
18879
|
+
The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
|
|
18880
|
+
);
|
|
18881
|
+
}
|
|
18882
|
+
return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
|
|
18883
|
+
}
|
|
18884
|
+
function loadKritikLibrary(root) {
|
|
18885
|
+
const pack = resolvePack(root);
|
|
18886
|
+
return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
|
|
18887
|
+
}
|
|
18888
|
+
function resolveJournal(root, bundlePath) {
|
|
18889
|
+
const resolved = bundlePath ?? join5(root, DEFAULT_BUNDLE_PATH5);
|
|
18890
|
+
return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync7(resolved) };
|
|
18891
|
+
}
|
|
18892
|
+
function appendQualityEvents(root, inputs, options = {}) {
|
|
18893
|
+
if (inputs.length === 0) return { events: [] };
|
|
18894
|
+
const actor = options.actor ?? KRITIK_ACTOR;
|
|
18895
|
+
const journal = resolveJournal(root, options.bundlePath);
|
|
18896
|
+
if (!journal.present) return { events: [] };
|
|
18897
|
+
const bundle = readBundle(journal.bundlePath);
|
|
18898
|
+
const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
|
|
18899
|
+
const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
|
|
18900
|
+
for (const event of events) appendJournalEvent(journal.journalPath, event);
|
|
18901
|
+
return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
|
|
18902
|
+
}
|
|
18903
|
+
function isQualitySection(value) {
|
|
18904
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18905
|
+
}
|
|
18906
|
+
function foldQualitySection(bundle, root, auditId) {
|
|
18907
|
+
const notFolded = (reason) => {
|
|
18908
|
+
const carriedSection = isQualitySection(bundle.quality);
|
|
18909
|
+
return {
|
|
18910
|
+
folded: false,
|
|
18911
|
+
carriedSection,
|
|
18912
|
+
notice: carriedSection ? `Quality: ${reason}
|
|
18913
|
+
Kept the quality section this bundle already carried \u2014 nothing replaced it.` : `Quality: ${reason}`
|
|
18914
|
+
};
|
|
18915
|
+
};
|
|
18916
|
+
if (auditId !== void 0) requireAudit(root, auditId);
|
|
18917
|
+
const auditIds = listAuditIds(root);
|
|
18918
|
+
if (auditIds.length === 0) {
|
|
18919
|
+
return notFolded(`none to fold \u2014 no audits under ${auditsDir(root)} (run \`arkaik kritik score\` to open one)`);
|
|
18920
|
+
}
|
|
18921
|
+
if (!loadProfile(root)) {
|
|
18922
|
+
return notFolded(`skipped, no profile \u2014 nothing at ${profilePath(root)} (run \`arkaik kritik profile\`)`);
|
|
18923
|
+
}
|
|
18924
|
+
let library;
|
|
18925
|
+
try {
|
|
18926
|
+
({ library } = loadKritikLibrary(root));
|
|
18927
|
+
} catch (e) {
|
|
18928
|
+
return notFolded(`skipped, no pack \u2014 ${e.message}`);
|
|
18929
|
+
}
|
|
18930
|
+
const section = auditId === void 0 ? loadCurrentQualitySection(root, library) : loadAuditQualitySection(root, auditId, library);
|
|
18931
|
+
if (section === void 0) {
|
|
18932
|
+
return notFolded(`nothing to fold \u2014 no audits under ${auditsDir(root)}, or no profile at ${profilePath(root)}`);
|
|
18933
|
+
}
|
|
18934
|
+
bundle.quality = section;
|
|
18935
|
+
const count = auditId === void 0 ? auditIds.length : 1;
|
|
18936
|
+
return {
|
|
18937
|
+
folded: true,
|
|
18938
|
+
carriedSection: false,
|
|
18939
|
+
notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
|
|
18940
|
+
};
|
|
18941
|
+
}
|
|
18942
|
+
function resolveQualityRoot(options) {
|
|
18943
|
+
if (options.root !== void 0) return resolve4(options.fallback, options.root);
|
|
18944
|
+
const envRoot = (options.env ?? process.env).ARKAIK_QUALITY_ROOT;
|
|
18945
|
+
if (envRoot) return resolve4(envRoot);
|
|
18946
|
+
const dir = dirname4(options.bundlePath);
|
|
18947
|
+
if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
|
|
18948
|
+
return options.fallback;
|
|
18949
|
+
}
|
|
18950
|
+
|
|
18951
|
+
// src/commands/pack.ts
|
|
18952
|
+
var DEFAULT_BUNDLE_PATH6 = "docs/arkaik/bundle.json";
|
|
18953
|
+
var USAGE7 = `arkaik pack [--no-journal] [--no-quality] [--inline-assets] [--audit <id>]
|
|
18954
|
+
[--root <dir>] [--out <path>] [path]
|
|
18583
18955
|
|
|
18584
18956
|
Produce a single self-contained interchange bundle: fold in the sidecar
|
|
18585
|
-
journal (or keep an existing embedded one)
|
|
18586
|
-
|
|
18587
|
-
|
|
18957
|
+
journal (or keep an existing embedded one), fold docs/quality/ into the
|
|
18958
|
+
quality section, and, with --inline-assets, inline local screenshot files as
|
|
18959
|
+
data: URIs. Written canonically via serializeBundle. Unknown top-level keys
|
|
18960
|
+
and unknown fields always round-trip.
|
|
18588
18961
|
|
|
18589
18962
|
Arguments:
|
|
18590
|
-
path Path to the bundle JSON file (default: ${
|
|
18963
|
+
path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH6}).
|
|
18591
18964
|
|
|
18592
18965
|
Options:
|
|
18593
18966
|
--no-journal Omit the embedded journal[] (Publik-safe posture \u2014 history
|
|
@@ -18596,12 +18969,33 @@ Options:
|
|
|
18596
18969
|
interchange) \u2014 embedded wins over the sidecar when the
|
|
18597
18970
|
bundle already carries one, otherwise the sidecar is used
|
|
18598
18971
|
(same precedence "arkaik validate" folds by).
|
|
18972
|
+
--no-quality DELETE the quality section rather than folding one in \u2014
|
|
18973
|
+
not merely "skip the fold", because the source bundle may
|
|
18974
|
+
already carry a section of its own and that one goes too.
|
|
18975
|
+
For any bundle that must not travel with open findings:
|
|
18976
|
+
a finding names an unfixed vulnerability and the file to
|
|
18977
|
+
find it in. ("arkaik push" packs this way by default;
|
|
18978
|
+
its --include-quality opts back in.)
|
|
18979
|
+
Default: docs/quality/ IS folded in.
|
|
18599
18980
|
--inline-assets Convert relative-path metadata.platformScreenshots values
|
|
18600
18981
|
into data: URIs by reading the file from disk (resolved
|
|
18601
18982
|
against the bundle's directory). Absolute https:// URLs
|
|
18602
18983
|
and existing data: URIs are left as-is. v1 scope: local
|
|
18603
18984
|
files only \u2014 uploading a remote/hosted copy is not
|
|
18604
18985
|
implemented.
|
|
18986
|
+
--audit <id> Pin ONE audit's snapshot instead of the default merge.
|
|
18987
|
+
They answer different questions: the merge (every audit,
|
|
18988
|
+
latest score per criterion x surface) answers "where does
|
|
18989
|
+
the product stand", while a pinned audit answers "how did
|
|
18990
|
+
that audit go" \u2014 the question its own matrix.json
|
|
18991
|
+
answers. An id that is not on disk is an error, not an
|
|
18992
|
+
empty section.
|
|
18993
|
+
--root <dir> Where docs/quality/ lives. Default: NOT the current
|
|
18994
|
+
directory \u2014 it is derived from the bundle's own path, so
|
|
18995
|
+
packing <repo>/docs/arkaik/bundle.json folds <repo>'s
|
|
18996
|
+
audits whatever directory you run from. Only a bundle
|
|
18997
|
+
kept outside that conventional layout falls back to the
|
|
18998
|
+
cwd, and that is the case this flag is for.
|
|
18605
18999
|
--out <path> Write the packed bundle here instead of stdout.
|
|
18606
19000
|
-h, --help Show this help.`;
|
|
18607
19001
|
function fail7(message) {
|
|
@@ -18639,7 +19033,7 @@ function fatalResult2(bundlePath, message) {
|
|
|
18639
19033
|
}
|
|
18640
19034
|
function runPack(options = {}) {
|
|
18641
19035
|
const cwd = options.cwd ?? process.cwd();
|
|
18642
|
-
const filePath =
|
|
19036
|
+
const filePath = resolve5(cwd, options.path ?? DEFAULT_BUNDLE_PATH6);
|
|
18643
19037
|
const noJournal = options.noJournal ?? false;
|
|
18644
19038
|
const inlineAssets = options.inlineAssets ?? false;
|
|
18645
19039
|
let bundle;
|
|
@@ -18660,10 +19054,24 @@ function runPack(options = {}) {
|
|
|
18660
19054
|
journalEventCount = events.length;
|
|
18661
19055
|
}
|
|
18662
19056
|
}
|
|
19057
|
+
let qualityFolded;
|
|
19058
|
+
let qualityNotice;
|
|
19059
|
+
if (options.noQuality ?? false) {
|
|
19060
|
+
delete bundle.quality;
|
|
19061
|
+
} else {
|
|
19062
|
+
const root = resolveQualityRoot({ root: options.root, bundlePath: filePath, fallback: cwd });
|
|
19063
|
+
try {
|
|
19064
|
+
const fold = foldQualitySection(bundle, root, options.audit);
|
|
19065
|
+
qualityFolded = fold.folded;
|
|
19066
|
+
qualityNotice = fold.notice;
|
|
19067
|
+
} catch (e) {
|
|
19068
|
+
return fatalResult2(filePath, e.message);
|
|
19069
|
+
}
|
|
19070
|
+
}
|
|
18663
19071
|
const inlinedAssets = [];
|
|
18664
19072
|
const assetWarnings = [];
|
|
18665
19073
|
if (inlineAssets) {
|
|
18666
|
-
const bundleDir =
|
|
19074
|
+
const bundleDir = dirname5(filePath);
|
|
18667
19075
|
const nodes = Array.isArray(bundle.nodes) ? bundle.nodes : [];
|
|
18668
19076
|
for (const node of nodes) {
|
|
18669
19077
|
const nodeId = typeof node.id === "string" ? node.id : "?";
|
|
@@ -18674,12 +19082,12 @@ function runPack(options = {}) {
|
|
|
18674
19082
|
const map2 = screenshots;
|
|
18675
19083
|
for (const [platform, value] of Object.entries(map2)) {
|
|
18676
19084
|
if (typeof value !== "string" || !isRelativeAssetPath(value)) continue;
|
|
18677
|
-
const assetPath =
|
|
18678
|
-
if (!
|
|
19085
|
+
const assetPath = resolve5(bundleDir, value);
|
|
19086
|
+
if (!existsSync8(assetPath)) {
|
|
18679
19087
|
assetWarnings.push(`${nodeId}/${platform}: asset not found at ${assetPath} \u2014 left as-is`);
|
|
18680
19088
|
continue;
|
|
18681
19089
|
}
|
|
18682
|
-
const bytes =
|
|
19090
|
+
const bytes = readFileSync7(assetPath);
|
|
18683
19091
|
const mime = mimeForExtension(extname(assetPath));
|
|
18684
19092
|
map2[platform] = `data:${mime};base64,${bytes.toString("base64")}`;
|
|
18685
19093
|
inlinedAssets.push({ nodeId, platform, path: value });
|
|
@@ -18689,15 +19097,18 @@ function runPack(options = {}) {
|
|
|
18689
19097
|
const output = serializeBundle(bundle);
|
|
18690
19098
|
let outPath;
|
|
18691
19099
|
if (options.out !== void 0) {
|
|
18692
|
-
outPath =
|
|
18693
|
-
|
|
18694
|
-
|
|
19100
|
+
outPath = resolve5(cwd, options.out);
|
|
19101
|
+
mkdirSync4(dirname5(outPath), { recursive: true });
|
|
19102
|
+
writeFileSync6(outPath, output);
|
|
18695
19103
|
}
|
|
18696
|
-
return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, output };
|
|
19104
|
+
return { ok: true, bundlePath: filePath, outPath, journalIncluded, journalEventCount, inlinedAssets, assetWarnings, qualityFolded, qualityNotice, output };
|
|
18697
19105
|
}
|
|
18698
19106
|
function runPackCli(args) {
|
|
18699
19107
|
let noJournal = false;
|
|
19108
|
+
let noQuality = false;
|
|
18700
19109
|
let inlineAssets = false;
|
|
19110
|
+
let audit;
|
|
19111
|
+
let root;
|
|
18701
19112
|
let out;
|
|
18702
19113
|
const positionals = [];
|
|
18703
19114
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -18707,8 +19118,22 @@ function runPackCli(args) {
|
|
|
18707
19118
|
process.exit(0);
|
|
18708
19119
|
} else if (arg === "--no-journal") {
|
|
18709
19120
|
noJournal = true;
|
|
19121
|
+
} else if (arg === "--no-quality") {
|
|
19122
|
+
noQuality = true;
|
|
18710
19123
|
} else if (arg === "--inline-assets") {
|
|
18711
19124
|
inlineAssets = true;
|
|
19125
|
+
} else if (arg === "--audit") {
|
|
19126
|
+
const value = args[++i];
|
|
19127
|
+
if (value === void 0) fail7(`Missing value for --audit
|
|
19128
|
+
|
|
19129
|
+
${USAGE7}`);
|
|
19130
|
+
audit = value;
|
|
19131
|
+
} else if (arg === "--root") {
|
|
19132
|
+
const value = args[++i];
|
|
19133
|
+
if (value === void 0) fail7(`Missing value for --root
|
|
19134
|
+
|
|
19135
|
+
${USAGE7}`);
|
|
19136
|
+
root = value;
|
|
18712
19137
|
} else if (arg === "--out") {
|
|
18713
19138
|
const value = args[++i];
|
|
18714
19139
|
if (value === void 0) fail7(`Missing value for --out
|
|
@@ -18723,8 +19148,13 @@ ${USAGE7}`);
|
|
|
18723
19148
|
positionals.push(arg);
|
|
18724
19149
|
}
|
|
18725
19150
|
}
|
|
18726
|
-
|
|
18727
|
-
|
|
19151
|
+
if (noQuality && audit !== void 0) {
|
|
19152
|
+
fail7(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
|
|
19153
|
+
|
|
19154
|
+
${USAGE7}`);
|
|
19155
|
+
}
|
|
19156
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH6;
|
|
19157
|
+
const result = runPack({ path: filePath, out, noJournal, inlineAssets, noQuality, audit, root });
|
|
18728
19158
|
if (!result.ok) fail7(`FATAL: ${result.fatal}`);
|
|
18729
19159
|
if (result.journalIncluded) {
|
|
18730
19160
|
console.error(`Journal: embedded ${result.journalEventCount} event(s)`);
|
|
@@ -18733,6 +19163,9 @@ ${USAGE7}`);
|
|
|
18733
19163
|
} else {
|
|
18734
19164
|
console.error("Journal: none to embed (no embedded journal, no sidecar)");
|
|
18735
19165
|
}
|
|
19166
|
+
if (result.qualityNotice !== void 0) {
|
|
19167
|
+
console.error(result.qualityNotice);
|
|
19168
|
+
}
|
|
18736
19169
|
for (const asset of result.inlinedAssets) {
|
|
18737
19170
|
console.error(`Inlined asset: ${asset.nodeId}/${asset.platform} (${asset.path})`);
|
|
18738
19171
|
}
|
|
@@ -18749,10 +19182,10 @@ ${USAGE7}`);
|
|
|
18749
19182
|
|
|
18750
19183
|
// src/commands/open.ts
|
|
18751
19184
|
import { spawn } from "node:child_process";
|
|
18752
|
-
import { mkdtempSync, writeFileSync as
|
|
19185
|
+
import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
18753
19186
|
import { tmpdir } from "node:os";
|
|
18754
|
-
import { join as
|
|
18755
|
-
var
|
|
19187
|
+
import { join as join6, resolve as resolve6 } from "node:path";
|
|
19188
|
+
var DEFAULT_BUNDLE_PATH7 = "docs/arkaik/bundle.json";
|
|
18756
19189
|
var OPEN_URL = "https://arkaik.app/projects";
|
|
18757
19190
|
var USAGE8 = `arkaik open [--out <path>] [--no-open] [path]
|
|
18758
19191
|
|
|
@@ -18763,7 +19196,7 @@ ${OPEN_URL} (the project list's "Import JSON" picker). On an invalid bundle,
|
|
|
18763
19196
|
findings are printed and nothing is packed, written, or opened.
|
|
18764
19197
|
|
|
18765
19198
|
Arguments:
|
|
18766
|
-
path Path to the bundle JSON file (default: ${
|
|
19199
|
+
path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH7}).
|
|
18767
19200
|
|
|
18768
19201
|
Options:
|
|
18769
19202
|
--out <path> Write the packed bundle here instead of a temp file.
|
|
@@ -18785,7 +19218,7 @@ function fatalResult3(bundlePath, message) {
|
|
|
18785
19218
|
}
|
|
18786
19219
|
async function runOpen(options = {}) {
|
|
18787
19220
|
const cwd = options.cwd ?? process.cwd();
|
|
18788
|
-
const filePath =
|
|
19221
|
+
const filePath = resolve6(cwd, options.path ?? DEFAULT_BUNDLE_PATH7);
|
|
18789
19222
|
const noOpen = options.noOpen ?? false;
|
|
18790
19223
|
let v;
|
|
18791
19224
|
try {
|
|
@@ -18804,9 +19237,9 @@ async function runOpen(options = {}) {
|
|
|
18804
19237
|
}
|
|
18805
19238
|
let outPath = packed.outPath;
|
|
18806
19239
|
if (outPath === void 0) {
|
|
18807
|
-
const dir = mkdtempSync(
|
|
18808
|
-
outPath =
|
|
18809
|
-
|
|
19240
|
+
const dir = mkdtempSync(join6(tmpdir(), "arkaik-open-"));
|
|
19241
|
+
outPath = join6(dir, "bundle.json");
|
|
19242
|
+
writeFileSync7(outPath, packed.output);
|
|
18810
19243
|
}
|
|
18811
19244
|
let opened = false;
|
|
18812
19245
|
if (!noOpen) {
|
|
@@ -18814,7 +19247,17 @@ async function runOpen(options = {}) {
|
|
|
18814
19247
|
await opener(OPEN_URL);
|
|
18815
19248
|
opened = true;
|
|
18816
19249
|
}
|
|
18817
|
-
return {
|
|
19250
|
+
return {
|
|
19251
|
+
ok: true,
|
|
19252
|
+
bundlePath: filePath,
|
|
19253
|
+
valid: true,
|
|
19254
|
+
errorLines,
|
|
19255
|
+
warningLines,
|
|
19256
|
+
outPath,
|
|
19257
|
+
url: OPEN_URL,
|
|
19258
|
+
opened,
|
|
19259
|
+
qualityNotice: packed.qualityNotice
|
|
19260
|
+
};
|
|
18818
19261
|
}
|
|
18819
19262
|
function runOpenCli(args) {
|
|
18820
19263
|
let out;
|
|
@@ -18841,7 +19284,7 @@ ${USAGE8}`);
|
|
|
18841
19284
|
positionals.push(arg);
|
|
18842
19285
|
}
|
|
18843
19286
|
}
|
|
18844
|
-
const filePath = positionals[0] ??
|
|
19287
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH7;
|
|
18845
19288
|
runOpen({ path: filePath, out, noOpen }).then((result) => {
|
|
18846
19289
|
if (!result.ok) fail8(`FATAL: ${result.fatal}`);
|
|
18847
19290
|
if (result.warningLines.length > 0) {
|
|
@@ -18854,6 +19297,9 @@ ${USAGE8}`);
|
|
|
18854
19297
|
console.error("\nInvalid bundle \u2014 not packed, not opened.");
|
|
18855
19298
|
process.exit(1);
|
|
18856
19299
|
}
|
|
19300
|
+
if (result.qualityNotice !== void 0) {
|
|
19301
|
+
console.error(result.qualityNotice);
|
|
19302
|
+
}
|
|
18857
19303
|
console.log(`Packed -> ${result.outPath}`);
|
|
18858
19304
|
if (result.opened) {
|
|
18859
19305
|
console.log(`Opened ${result.url}`);
|
|
@@ -18865,10 +19311,10 @@ ${USAGE8}`);
|
|
|
18865
19311
|
}
|
|
18866
19312
|
|
|
18867
19313
|
// src/commands/push.ts
|
|
18868
|
-
import { resolve as
|
|
18869
|
-
var
|
|
19314
|
+
import { resolve as resolve7 } from "node:path";
|
|
19315
|
+
var DEFAULT_BUNDLE_PATH8 = "docs/arkaik/bundle.json";
|
|
18870
19316
|
var DEFAULT_API_BASE = "https://arkaik.app";
|
|
18871
|
-
var USAGE9 = `arkaik push [--include-journal] [--api <base-url>] [path]
|
|
19317
|
+
var USAGE9 = `arkaik push [--include-journal] [--include-quality] [--api <base-url>] [path]
|
|
18872
19318
|
arkaik push --delete <id> --key <owner_key> [--api <base-url>]
|
|
18873
19319
|
|
|
18874
19320
|
Publish a project bundle to Publik (anonymous, account-less snapshot
|
|
@@ -18882,18 +19328,29 @@ Publik-safe posture (docs/spec/journal.md). --include-journal embeds it
|
|
|
18882
19328
|
(like a bare "arkaik pack") and forwards ?include_journal=true so the server
|
|
18883
19329
|
knows to keep it.
|
|
18884
19330
|
|
|
19331
|
+
A Kritik "quality" section is stripped by default too, and separately:
|
|
19332
|
+
publishing your history and publishing your open findings are two decisions,
|
|
19333
|
+
not one, and both default to no. --include-quality opts in.
|
|
19334
|
+
|
|
18885
19335
|
Snapshots are immutable: there is no update verb. Pushing again always mints
|
|
18886
19336
|
a new id. The owner key printed on success is shown exactly once and cannot
|
|
18887
19337
|
be recovered \u2014 save it if you may need to delete the snapshot later.
|
|
18888
19338
|
|
|
18889
19339
|
Arguments:
|
|
18890
|
-
path Path to the bundle JSON file (default: ${
|
|
19340
|
+
path Path to the bundle JSON file (default: ${DEFAULT_BUNDLE_PATH8}).
|
|
18891
19341
|
Ignored with --delete.
|
|
18892
19342
|
|
|
18893
19343
|
Options:
|
|
18894
19344
|
--include-journal Embed the journal in the pushed bundle and forward
|
|
18895
19345
|
?include_journal=true. Default: stripped, omitted
|
|
18896
19346
|
entirely from the request body.
|
|
19347
|
+
--include-quality Embed the Kritik quality section and forward
|
|
19348
|
+
?include_quality=true. Opt-in rather than opt-out
|
|
19349
|
+
because an open finding is an unfixed vulnerability
|
|
19350
|
+
plus the path to find it in (docs/rfcs/kritik.md
|
|
19351
|
+
\xA7 8.3) \u2014 publishing that is a decision worth typing.
|
|
19352
|
+
Default: deleted before packing, so it is never in the
|
|
19353
|
+
request body at all.
|
|
18897
19354
|
--api <base-url> Publik API base URL (default: ${DEFAULT_API_BASE}).
|
|
18898
19355
|
Point at a self-hosted deployment.
|
|
18899
19356
|
--delete <id> Delete a snapshot by id instead of pushing. Requires
|
|
@@ -18918,8 +19375,9 @@ function fatalResult4(bundlePath, message) {
|
|
|
18918
19375
|
}
|
|
18919
19376
|
async function runPush(options = {}) {
|
|
18920
19377
|
const cwd = options.cwd ?? process.cwd();
|
|
18921
|
-
const filePath =
|
|
19378
|
+
const filePath = resolve7(cwd, options.path ?? DEFAULT_BUNDLE_PATH8);
|
|
18922
19379
|
const includeJournal = options.includeJournal ?? false;
|
|
19380
|
+
const includeQuality = options.includeQuality ?? false;
|
|
18923
19381
|
const apiBase = options.apiBase ?? DEFAULT_API_BASE;
|
|
18924
19382
|
const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
|
|
18925
19383
|
let v;
|
|
@@ -18933,11 +19391,15 @@ async function runPush(options = {}) {
|
|
|
18933
19391
|
if (!v.valid) {
|
|
18934
19392
|
return { ok: true, bundlePath: filePath, valid: false, errorLines, warningLines, requestSent: false };
|
|
18935
19393
|
}
|
|
18936
|
-
const packed = runPack({ path: filePath, noJournal: !includeJournal, cwd });
|
|
19394
|
+
const packed = runPack({ path: filePath, noJournal: !includeJournal, noQuality: !includeQuality, cwd });
|
|
18937
19395
|
if (!packed.ok) {
|
|
18938
19396
|
return fatalResult4(filePath, packed.fatal ?? "pack failed");
|
|
18939
19397
|
}
|
|
18940
|
-
const
|
|
19398
|
+
const qualityNotice = packed.qualityNotice ?? "Quality: stripped, not sent (pass --include-quality to publish it)";
|
|
19399
|
+
const params = [];
|
|
19400
|
+
if (includeJournal) params.push("include_journal=true");
|
|
19401
|
+
if (includeQuality) params.push("include_quality=true");
|
|
19402
|
+
const url2 = `${apiBase}/api/publik${params.length > 0 ? `?${params.join("&")}` : ""}`;
|
|
18941
19403
|
let res;
|
|
18942
19404
|
try {
|
|
18943
19405
|
res = await httpClient(url2, {
|
|
@@ -18967,6 +19429,7 @@ async function runPush(options = {}) {
|
|
|
18967
19429
|
warningLines,
|
|
18968
19430
|
requestSent: true,
|
|
18969
19431
|
status,
|
|
19432
|
+
qualityNotice,
|
|
18970
19433
|
id: body.id,
|
|
18971
19434
|
url: body.url,
|
|
18972
19435
|
ownerKey: body.owner_key
|
|
@@ -18985,6 +19448,7 @@ async function runPush(options = {}) {
|
|
|
18985
19448
|
warningLines,
|
|
18986
19449
|
requestSent: true,
|
|
18987
19450
|
status,
|
|
19451
|
+
qualityNotice,
|
|
18988
19452
|
serverFindings: errBody.findings,
|
|
18989
19453
|
retryAfter: status === 429 ? res.headers.get("retry-after") : void 0,
|
|
18990
19454
|
errorMessage: errBody.message ?? `Request failed with status ${status}`
|
|
@@ -18995,6 +19459,9 @@ function reportPush(result) {
|
|
|
18995
19459
|
console.error(`Warnings: ${result.warningLines.length}`);
|
|
18996
19460
|
result.warningLines.forEach((w) => console.error(` ${w}`));
|
|
18997
19461
|
}
|
|
19462
|
+
if (result.qualityNotice !== void 0) {
|
|
19463
|
+
console.error(result.qualityNotice);
|
|
19464
|
+
}
|
|
18998
19465
|
if (!result.valid) {
|
|
18999
19466
|
console.error(`Errors: ${result.errorLines.length}`);
|
|
19000
19467
|
result.errorLines.forEach((e) => console.error(` ${e}`));
|
|
@@ -19076,6 +19543,7 @@ function reportDelete(id, result) {
|
|
|
19076
19543
|
}
|
|
19077
19544
|
function runPushCli(args) {
|
|
19078
19545
|
let includeJournal = false;
|
|
19546
|
+
let includeQuality = false;
|
|
19079
19547
|
let apiBase;
|
|
19080
19548
|
let deleteId;
|
|
19081
19549
|
let key;
|
|
@@ -19087,6 +19555,8 @@ function runPushCli(args) {
|
|
|
19087
19555
|
process.exit(0);
|
|
19088
19556
|
} else if (arg === "--include-journal") {
|
|
19089
19557
|
includeJournal = true;
|
|
19558
|
+
} else if (arg === "--include-quality") {
|
|
19559
|
+
includeQuality = true;
|
|
19090
19560
|
} else if (arg === "--api") {
|
|
19091
19561
|
const value = args[++i];
|
|
19092
19562
|
if (value === void 0) fail9(`Missing value for --api
|
|
@@ -19128,16 +19598,16 @@ ${USAGE9}`);
|
|
|
19128
19598
|
if (key !== void 0) fail9(`--key is only valid with --delete
|
|
19129
19599
|
|
|
19130
19600
|
${USAGE9}`);
|
|
19131
|
-
const filePath = positionals[0] ??
|
|
19132
|
-
runPush({ path: filePath, includeJournal, apiBase }).then((result) => {
|
|
19601
|
+
const filePath = positionals[0] ?? DEFAULT_BUNDLE_PATH8;
|
|
19602
|
+
runPush({ path: filePath, includeJournal, includeQuality, apiBase }).then((result) => {
|
|
19133
19603
|
if (!result.ok) fail9(`FATAL: ${result.fatal}`);
|
|
19134
19604
|
reportPush(result);
|
|
19135
19605
|
}).catch((e) => fail9(`FATAL: ${e.message}`));
|
|
19136
19606
|
}
|
|
19137
19607
|
|
|
19138
19608
|
// src/commands/link.ts
|
|
19139
|
-
import { mkdirSync as
|
|
19140
|
-
import { dirname as
|
|
19609
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
19610
|
+
import { dirname as dirname6, join as join7, resolve as resolve8 } from "node:path";
|
|
19141
19611
|
var LINK_FILE = "docs/arkaik/arkaik.json";
|
|
19142
19612
|
var DEFAULT_BASE_URL = "https://arkaik.app";
|
|
19143
19613
|
var USAGE10 = `arkaik link \u2014 point this repo at a hosted Arkaik project
|
|
@@ -19208,15 +19678,15 @@ async function runLink(argv, options = {}) {
|
|
|
19208
19678
|
return { ok: false };
|
|
19209
19679
|
}
|
|
19210
19680
|
const { bundle } = await res.json();
|
|
19211
|
-
const target =
|
|
19212
|
-
const linkPath =
|
|
19213
|
-
|
|
19681
|
+
const target = resolve8(cwd, argv.find((a) => !a.startsWith("--") && a !== projectId && a !== baseUrl) ?? ".");
|
|
19682
|
+
const linkPath = join7(target, LINK_FILE);
|
|
19683
|
+
mkdirSync5(dirname6(linkPath), { recursive: true });
|
|
19214
19684
|
let existing = {};
|
|
19215
19685
|
try {
|
|
19216
|
-
existing = JSON.parse(
|
|
19686
|
+
existing = JSON.parse(readFileSync8(linkPath, "utf8"));
|
|
19217
19687
|
} catch {
|
|
19218
19688
|
}
|
|
19219
|
-
|
|
19689
|
+
writeFileSync8(
|
|
19220
19690
|
linkPath,
|
|
19221
19691
|
`${JSON.stringify({ ...existing, project_id: projectId, remote: baseUrl }, null, 2)}
|
|
19222
19692
|
`
|
|
@@ -19243,14 +19713,15 @@ function runLinkCli(argv) {
|
|
|
19243
19713
|
}
|
|
19244
19714
|
|
|
19245
19715
|
// src/commands/restore.ts
|
|
19246
|
-
import { existsSync as
|
|
19247
|
-
import { join as
|
|
19716
|
+
import { existsSync as existsSync9, linkSync, mkdirSync as mkdirSync6, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
19717
|
+
import { join as join8, resolve as resolve9 } from "node:path";
|
|
19248
19718
|
var LINK_FILE2 = "docs/arkaik/arkaik.json";
|
|
19249
|
-
var
|
|
19719
|
+
var DEFAULT_BUNDLE_PATH9 = "docs/arkaik/bundle.json";
|
|
19250
19720
|
var DEFAULT_API_BASE2 = "https://arkaik.app";
|
|
19251
19721
|
var USAGE11 = `arkaik restore [options] [path]
|
|
19252
19722
|
|
|
19253
|
-
Replace the linked hosted project's bundle
|
|
19723
|
+
Replace the linked hosted project's bundle, journal AND quality section with
|
|
19724
|
+
a local bundle \u2014
|
|
19254
19725
|
the landing step for a bootstrapped map. Before sending anything, this
|
|
19255
19726
|
exports the CURRENT hosted state (snapshot + journal) to
|
|
19256
19727
|
docs/arkaik/.backups/<timestamp>-bundle.json (next to the link file \u2014 not
|
|
@@ -19260,9 +19731,10 @@ only way back if the restore turns out to be wrong.
|
|
|
19260
19731
|
|
|
19261
19732
|
Arguments:
|
|
19262
19733
|
path Path to the local bundle JSON file
|
|
19263
|
-
(default: ${
|
|
19734
|
+
(default: ${DEFAULT_BUNDLE_PATH9}). Its journal.jsonl
|
|
19264
19735
|
sidecar (or an embedded journal, which wins) is folded
|
|
19265
|
-
in automatically
|
|
19736
|
+
in automatically, as is the docs/quality/ tree of the
|
|
19737
|
+
repo that bundle belongs to.
|
|
19266
19738
|
|
|
19267
19739
|
Options:
|
|
19268
19740
|
--dry-run Ask the server what this restore WOULD do and print
|
|
@@ -19282,6 +19754,25 @@ Options:
|
|
|
19282
19754
|
local copy (edited in the app), not an intended
|
|
19283
19755
|
deletion. Undoing a restore from a backup is the
|
|
19284
19756
|
common case where it IS intended.
|
|
19757
|
+
--no-quality Do not send a quality section: skip the docs/quality/
|
|
19758
|
+
fold AND drop any section the local bundle already
|
|
19759
|
+
carries. Does NOT by itself permit erasing the hosted
|
|
19760
|
+
one \u2014 that needs --allow-quality-loss too, because
|
|
19761
|
+
"don't send mine" and "destroy theirs" are different
|
|
19762
|
+
decisions and only one of them is irreversible.
|
|
19763
|
+
--allow-quality-loss Proceed even though the restore would erase a quality
|
|
19764
|
+
section the hosted project currently has. Without this
|
|
19765
|
+
flag, that refuses outright \u2014 it usually means
|
|
19766
|
+
docs/quality/ was looked for in the wrong place, which
|
|
19767
|
+
--root fixes, rather than an intended wipe.
|
|
19768
|
+
--audit <id> Fold ONE audit's snapshot rather than merging every
|
|
19769
|
+
audit into current state. An id that is not on disk is
|
|
19770
|
+
an error, not an empty section.
|
|
19771
|
+
--root <dir> Where docs/quality/ lives. Default: derived from the
|
|
19772
|
+
bundle's own path, so restoring <repo>/docs/arkaik/
|
|
19773
|
+
bundle.json folds <repo>'s audits whatever directory
|
|
19774
|
+
you run from; only a bundle kept outside that layout
|
|
19775
|
+
falls back to the cwd.
|
|
19285
19776
|
--api <base-url> Override the remote from docs/arkaik/arkaik.json
|
|
19286
19777
|
(also overridable with $ARKAIK_URL).
|
|
19287
19778
|
-h, --help Show this help.
|
|
@@ -19294,8 +19785,8 @@ function fail10(message) {
|
|
|
19294
19785
|
console.error(message);
|
|
19295
19786
|
process.exit(1);
|
|
19296
19787
|
}
|
|
19297
|
-
function fatalResult5(dryRun, message) {
|
|
19298
|
-
return { ok: false, fatal: message, dryRun, requestSent: false };
|
|
19788
|
+
function fatalResult5(dryRun, message, quality = {}) {
|
|
19789
|
+
return { ok: false, fatal: message, dryRun, requestSent: false, ...quality };
|
|
19299
19790
|
}
|
|
19300
19791
|
async function safeJson(res) {
|
|
19301
19792
|
try {
|
|
@@ -19321,7 +19812,9 @@ async function interpretPutResponse(res, ctx) {
|
|
|
19321
19812
|
bundlePath: ctx.bundlePath,
|
|
19322
19813
|
backupPath: ctx.backupPath,
|
|
19323
19814
|
requestSent: true,
|
|
19324
|
-
status: res.status
|
|
19815
|
+
status: res.status,
|
|
19816
|
+
qualityFolded: ctx.qualityFolded,
|
|
19817
|
+
qualityNotice: ctx.qualityNotice
|
|
19325
19818
|
};
|
|
19326
19819
|
const backupNote = backupNoteFor(ctx.backupPath);
|
|
19327
19820
|
if (res.status === 200) {
|
|
@@ -19385,7 +19878,7 @@ async function interpretPutResponse(res, ctx) {
|
|
|
19385
19878
|
}
|
|
19386
19879
|
function writeBackupFile(filePath, content) {
|
|
19387
19880
|
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
19388
|
-
|
|
19881
|
+
writeFileSync9(tmpPath, content);
|
|
19389
19882
|
try {
|
|
19390
19883
|
linkSync(tmpPath, filePath);
|
|
19391
19884
|
} finally {
|
|
@@ -19426,20 +19919,50 @@ function describeDeletions(removedNodes, removedEdges, bundlePath) {
|
|
|
19426
19919
|
);
|
|
19427
19920
|
return lines.join("\n");
|
|
19428
19921
|
}
|
|
19922
|
+
function describeQualityLoss(hostedQuality, qualityRoot, noQuality, foldNotice) {
|
|
19923
|
+
const findings = Array.isArray(hostedQuality.findings) ? hostedQuality.findings.length : 0;
|
|
19924
|
+
const assessments = Array.isArray(hostedQuality.assessments) ? hostedQuality.assessments.length : 0;
|
|
19925
|
+
const parts = [];
|
|
19926
|
+
if (findings > 0) parts.push(`${findings} open finding${findings === 1 ? "" : "s"}`);
|
|
19927
|
+
if (assessments > 0) parts.push(`${assessments} assessment${assessments === 1 ? "" : "s"}`);
|
|
19928
|
+
const held = parts.length > 0 ? parts.join(" and ") : "no findings or assessments";
|
|
19929
|
+
const lines = [
|
|
19930
|
+
`This restore would ERASE the hosted project's quality section (${held}). Nothing was sent.`
|
|
19931
|
+
];
|
|
19932
|
+
if (noQuality) {
|
|
19933
|
+
lines.push(
|
|
19934
|
+
`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.`
|
|
19935
|
+
);
|
|
19936
|
+
} else {
|
|
19937
|
+
if (foldNotice !== void 0) {
|
|
19938
|
+
lines.push("The outbound bundle has no section because the fold found nothing to build one from:");
|
|
19939
|
+
lines.push(` ${foldNotice}`);
|
|
19940
|
+
}
|
|
19941
|
+
if (!existsSync9(join8(qualityRoot, QUALITY_DIR))) {
|
|
19942
|
+
lines.push(
|
|
19943
|
+
`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.`
|
|
19944
|
+
);
|
|
19945
|
+
}
|
|
19946
|
+
lines.push(`If the hosted section really is meant to go, re-run with --allow-quality-loss.`);
|
|
19947
|
+
}
|
|
19948
|
+
return lines.join("\n");
|
|
19949
|
+
}
|
|
19429
19950
|
async function runRestore(options = {}) {
|
|
19430
19951
|
const cwd = options.cwd ?? process.cwd();
|
|
19431
19952
|
const env = options.env ?? process.env;
|
|
19432
19953
|
const dryRun = options.dryRun ?? false;
|
|
19433
19954
|
const allowHistoryLoss = options.allowHistoryLoss ?? false;
|
|
19434
19955
|
const allowDeletions = options.allowDeletions ?? false;
|
|
19956
|
+
const noQuality = options.noQuality ?? false;
|
|
19957
|
+
const allowQualityLoss = options.allowQualityLoss ?? false;
|
|
19435
19958
|
const httpClient = options.httpClient ?? DEFAULT_HTTP_CLIENT;
|
|
19436
|
-
const linkPath =
|
|
19437
|
-
if (!
|
|
19959
|
+
const linkPath = join8(cwd, LINK_FILE2);
|
|
19960
|
+
if (!existsSync9(linkPath)) {
|
|
19438
19961
|
return fatalResult5(dryRun, `No ${LINK_FILE2}. Run \`arkaik link\` first \u2014 restore only targets hosted projects.`);
|
|
19439
19962
|
}
|
|
19440
19963
|
let link;
|
|
19441
19964
|
try {
|
|
19442
|
-
link = JSON.parse(
|
|
19965
|
+
link = JSON.parse(readFileSync9(linkPath, "utf8"));
|
|
19443
19966
|
} catch (e) {
|
|
19444
19967
|
return fatalResult5(dryRun, `Could not parse ${LINK_FILE2}: ${e.message}`);
|
|
19445
19968
|
}
|
|
@@ -19449,11 +19972,11 @@ async function runRestore(options = {}) {
|
|
|
19449
19972
|
const encodedProjectId = encodeURIComponent(projectId);
|
|
19450
19973
|
const token = env.ARKAIK_TOKEN;
|
|
19451
19974
|
if (!token) return fatalResult5(dryRun, `ARKAIK_TOKEN is not set. Create a token at ${baseUrl}/settings/tokens and export it.`);
|
|
19452
|
-
const bundlePath =
|
|
19453
|
-
if (!
|
|
19975
|
+
const bundlePath = resolve9(cwd, options.path ?? DEFAULT_BUNDLE_PATH9);
|
|
19976
|
+
if (!existsSync9(bundlePath)) return fatalResult5(dryRun, `No bundle at ${bundlePath}. Run \`arkaik merge\` (or \`arkaik pack\`) first.`);
|
|
19454
19977
|
let localRaw;
|
|
19455
19978
|
try {
|
|
19456
|
-
localRaw = JSON.parse(
|
|
19979
|
+
localRaw = JSON.parse(readFileSync9(bundlePath, "utf8"));
|
|
19457
19980
|
} catch (e) {
|
|
19458
19981
|
return fatalResult5(dryRun, `Could not parse ${bundlePath}: ${e.message}`);
|
|
19459
19982
|
}
|
|
@@ -19463,6 +19986,22 @@ async function runRestore(options = {}) {
|
|
|
19463
19986
|
const local = localRaw;
|
|
19464
19987
|
const journalEvents = loadJournalEvents(local, bundlePath);
|
|
19465
19988
|
const outboundBundle = { ...local, journal: journalEvents };
|
|
19989
|
+
const qualityRoot = resolveQualityRoot({ root: options.root, bundlePath, fallback: cwd });
|
|
19990
|
+
let qualityFolded = false;
|
|
19991
|
+
let qualityNotice;
|
|
19992
|
+
if (noQuality) {
|
|
19993
|
+
delete outboundBundle.quality;
|
|
19994
|
+
qualityNotice = "Quality: deleted before sending (--no-quality)";
|
|
19995
|
+
} else {
|
|
19996
|
+
try {
|
|
19997
|
+
const fold = foldQualitySection(outboundBundle, qualityRoot, options.audit);
|
|
19998
|
+
qualityFolded = fold.folded;
|
|
19999
|
+
qualityNotice = fold.notice;
|
|
20000
|
+
} catch (e) {
|
|
20001
|
+
return fatalResult5(dryRun, e.message);
|
|
20002
|
+
}
|
|
20003
|
+
}
|
|
20004
|
+
const qualityFields = { qualityFolded, qualityNotice };
|
|
19466
20005
|
const headers = { Authorization: `Bearer ${token}` };
|
|
19467
20006
|
let version2;
|
|
19468
20007
|
try {
|
|
@@ -19491,7 +20030,7 @@ async function runRestore(options = {}) {
|
|
|
19491
20030
|
} catch (e) {
|
|
19492
20031
|
return { ok: true, dryRun, bundlePath, requestSent: false, errorMessage: `Network error: ${e.message}` };
|
|
19493
20032
|
}
|
|
19494
|
-
return interpretPutResponse(res2, { dryRun, bundlePath, version: version2 });
|
|
20033
|
+
return interpretPutResponse(res2, { dryRun, bundlePath, version: version2, ...qualityFields });
|
|
19495
20034
|
}
|
|
19496
20035
|
let exported;
|
|
19497
20036
|
try {
|
|
@@ -19518,28 +20057,34 @@ async function runRestore(options = {}) {
|
|
|
19518
20057
|
if (journalEvents.length < hostedEventCount && !allowHistoryLoss) {
|
|
19519
20058
|
return fatalResult5(
|
|
19520
20059
|
dryRun,
|
|
19521
|
-
`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
|
|
20060
|
+
`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.`,
|
|
20061
|
+
qualityFields
|
|
19522
20062
|
);
|
|
19523
20063
|
}
|
|
20064
|
+
const hostedQuality = exportedBundle.quality;
|
|
20065
|
+
if (isQualitySection(hostedQuality) && !isQualitySection(outboundBundle.quality) && !allowQualityLoss) {
|
|
20066
|
+
return fatalResult5(dryRun, describeQualityLoss(hostedQuality, qualityRoot, noQuality, qualityNotice), qualityFields);
|
|
20067
|
+
}
|
|
19524
20068
|
const removedNodes = removedIds(exportedBundle.nodes, Array.isArray(local.nodes) ? local.nodes : []);
|
|
19525
20069
|
const removedEdges = removedIds(exportedBundle.edges, Array.isArray(local.edges) ? local.edges : []);
|
|
19526
20070
|
if ((removedNodes.length > 0 || removedEdges.length > 0) && !allowDeletions) {
|
|
19527
|
-
return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath));
|
|
20071
|
+
return fatalResult5(dryRun, describeDeletions(removedNodes, removedEdges, bundlePath), qualityFields);
|
|
19528
20072
|
}
|
|
19529
|
-
const backupDir =
|
|
20073
|
+
const backupDir = join8(cwd, "docs", "arkaik", ".backups");
|
|
19530
20074
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
19531
|
-
const backupPath =
|
|
20075
|
+
const backupPath = join8(backupDir, `${stamp}-bundle.json`);
|
|
19532
20076
|
const backupContent = `${JSON.stringify(exported, null, 2)}
|
|
19533
20077
|
`;
|
|
19534
20078
|
try {
|
|
19535
|
-
|
|
20079
|
+
mkdirSync6(backupDir, { recursive: true });
|
|
19536
20080
|
writeBackupFile(backupPath, backupContent);
|
|
19537
|
-
JSON.parse(
|
|
20081
|
+
JSON.parse(readFileSync9(backupPath, "utf8"));
|
|
19538
20082
|
} catch (e) {
|
|
19539
20083
|
return fatalResult5(
|
|
19540
20084
|
dryRun,
|
|
19541
20085
|
`Could not write the pre-restore backup to ${backupPath}: ${e.message}
|
|
19542
|
-
Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back
|
|
20086
|
+
Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND journal, and the backup is the only way back.`,
|
|
20087
|
+
qualityFields
|
|
19543
20088
|
);
|
|
19544
20089
|
}
|
|
19545
20090
|
let res;
|
|
@@ -19559,7 +20104,7 @@ Refusing to restore \u2014 this verb replaces the hosted project's snapshot AND
|
|
|
19559
20104
|
errorMessage: `Network error: ${e.message}. Nothing was sent.${backupNoteFor(backupPath)}`
|
|
19560
20105
|
};
|
|
19561
20106
|
}
|
|
19562
|
-
return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2 });
|
|
20107
|
+
return interpretPutResponse(res, { dryRun, bundlePath, backupPath, version: version2, ...qualityFields });
|
|
19563
20108
|
}
|
|
19564
20109
|
function printDelta(delta) {
|
|
19565
20110
|
if (!delta) return;
|
|
@@ -19590,6 +20135,9 @@ function reportRestore(result) {
|
|
|
19590
20135
|
if (result.backupPath) {
|
|
19591
20136
|
console.log(`Backed up the current hosted project (snapshot + journal) to ${result.backupPath}`);
|
|
19592
20137
|
}
|
|
20138
|
+
if (result.qualityNotice !== void 0) {
|
|
20139
|
+
console.log(result.qualityNotice);
|
|
20140
|
+
}
|
|
19593
20141
|
if (!result.requestSent) {
|
|
19594
20142
|
console.error(result.errorMessage ?? "Restore failed before a request could be sent.");
|
|
19595
20143
|
process.exit(1);
|
|
@@ -19598,7 +20146,9 @@ function reportRestore(result) {
|
|
|
19598
20146
|
if (result.dryRun) {
|
|
19599
20147
|
console.log("[dry-run] server preview \u2014 nothing was written:");
|
|
19600
20148
|
printDelta(result.delta);
|
|
19601
|
-
console.log(
|
|
20149
|
+
console.log(
|
|
20150
|
+
"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)."
|
|
20151
|
+
);
|
|
19602
20152
|
} else {
|
|
19603
20153
|
console.log(`Restored. New version ${result.version}.`);
|
|
19604
20154
|
printDelta(result.delta);
|
|
@@ -19619,6 +20169,10 @@ function runRestoreCli(argv) {
|
|
|
19619
20169
|
let dryRun = false;
|
|
19620
20170
|
let allowHistoryLoss = false;
|
|
19621
20171
|
let allowDeletions = false;
|
|
20172
|
+
let noQuality = false;
|
|
20173
|
+
let allowQualityLoss = false;
|
|
20174
|
+
let audit;
|
|
20175
|
+
let root;
|
|
19622
20176
|
let apiBase;
|
|
19623
20177
|
const positionals = [];
|
|
19624
20178
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -19633,6 +20187,22 @@ function runRestoreCli(argv) {
|
|
|
19633
20187
|
allowHistoryLoss = true;
|
|
19634
20188
|
} else if (arg === "--allow-deletions") {
|
|
19635
20189
|
allowDeletions = true;
|
|
20190
|
+
} else if (arg === "--no-quality") {
|
|
20191
|
+
noQuality = true;
|
|
20192
|
+
} else if (arg === "--allow-quality-loss") {
|
|
20193
|
+
allowQualityLoss = true;
|
|
20194
|
+
} else if (arg === "--audit") {
|
|
20195
|
+
const value = argv[++i];
|
|
20196
|
+
if (value === void 0) fail10(`Missing value for --audit
|
|
20197
|
+
|
|
20198
|
+
${USAGE11}`);
|
|
20199
|
+
audit = value;
|
|
20200
|
+
} else if (arg === "--root") {
|
|
20201
|
+
const value = argv[++i];
|
|
20202
|
+
if (value === void 0) fail10(`Missing value for --root
|
|
20203
|
+
|
|
20204
|
+
${USAGE11}`);
|
|
20205
|
+
root = value;
|
|
19636
20206
|
} else if (arg === "--api") {
|
|
19637
20207
|
const value = argv[++i];
|
|
19638
20208
|
if (value === void 0) fail10(`Missing value for --api
|
|
@@ -19650,28 +20220,33 @@ ${USAGE11}`);
|
|
|
19650
20220
|
if (positionals.length > 1) fail10(`Unexpected argument(s): ${positionals.slice(1).join(" ")}
|
|
19651
20221
|
|
|
19652
20222
|
${USAGE11}`);
|
|
19653
|
-
|
|
20223
|
+
if (noQuality && audit !== void 0) {
|
|
20224
|
+
fail10(`--audit and --no-quality contradict each other: one names an audit to fold, the other removes the section
|
|
20225
|
+
|
|
20226
|
+
${USAGE11}`);
|
|
20227
|
+
}
|
|
20228
|
+
runRestore({ path: positionals[0], dryRun, allowHistoryLoss, allowDeletions, noQuality, allowQualityLoss, audit, root, apiBase }).then((result) => reportRestore(result)).catch((e) => fail10(`FATAL: ${e.message}`));
|
|
19654
20229
|
}
|
|
19655
20230
|
|
|
19656
20231
|
// src/commands/bootstrap.ts
|
|
19657
20232
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
19658
|
-
import { existsSync as
|
|
20233
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync, writeFileSync as writeFileSync13 } from "node:fs";
|
|
19659
20234
|
import path5 from "node:path";
|
|
19660
20235
|
|
|
19661
20236
|
// src/lib/bootstrap/corpus.ts
|
|
19662
20237
|
import { spawnSync } from "node:child_process";
|
|
19663
|
-
import { existsSync as
|
|
20238
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
19664
20239
|
import path2 from "node:path";
|
|
19665
20240
|
|
|
19666
20241
|
// src/lib/bootstrap/paths.ts
|
|
19667
|
-
import { existsSync as
|
|
20242
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
|
|
19668
20243
|
import path from "node:path";
|
|
19669
20244
|
var BOOTSTRAP_ROOT = ".arkaik";
|
|
19670
20245
|
var CORPUS_DIR = ".arkaik/corpus";
|
|
19671
20246
|
var PLAN_DIR = ".arkaik/bootstrap";
|
|
19672
20247
|
var FRAGMENTS_DIR = ".arkaik/bootstrap/fragments";
|
|
19673
20248
|
var MANIFEST_FILE = ".arkaik/bootstrap/manifest.json";
|
|
19674
|
-
var
|
|
20249
|
+
var PROFILE_FILE2 = ".arkaik/bootstrap/profile.json";
|
|
19675
20250
|
var PRS_FILE = ".arkaik/corpus/prs.jsonl";
|
|
19676
20251
|
var DOCS_FILE = ".arkaik/corpus/docs.json";
|
|
19677
20252
|
var SURFACES_FILE = ".arkaik/corpus/surfaces.json";
|
|
@@ -19679,16 +20254,16 @@ function at(cwd, relative) {
|
|
|
19679
20254
|
return path.join(cwd, relative);
|
|
19680
20255
|
}
|
|
19681
20256
|
function ensureDir(dirPath) {
|
|
19682
|
-
|
|
20257
|
+
mkdirSync7(dirPath, { recursive: true });
|
|
19683
20258
|
}
|
|
19684
20259
|
function ensureGitignored(cwd) {
|
|
19685
20260
|
const file2 = path.join(cwd, ".gitignore");
|
|
19686
20261
|
const line2 = `${BOOTSTRAP_ROOT}/`;
|
|
19687
|
-
const current =
|
|
20262
|
+
const current = existsSync10(file2) ? readFileSync10(file2, "utf8") : "";
|
|
19688
20263
|
const ignored = current.split("\n").map((l) => l.trim()).some((l) => l === line2 || l === BOOTSTRAP_ROOT);
|
|
19689
20264
|
if (ignored) return false;
|
|
19690
20265
|
const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
|
19691
|
-
|
|
20266
|
+
writeFileSync10(file2, `${current}${prefix}${line2}
|
|
19692
20267
|
`);
|
|
19693
20268
|
return true;
|
|
19694
20269
|
}
|
|
@@ -19785,7 +20360,7 @@ function fetchPrsViaGit(cwd) {
|
|
|
19785
20360
|
function walk(root, cwd, out) {
|
|
19786
20361
|
let entries;
|
|
19787
20362
|
try {
|
|
19788
|
-
entries =
|
|
20363
|
+
entries = readdirSync3(root, { withFileTypes: true });
|
|
19789
20364
|
} catch {
|
|
19790
20365
|
return;
|
|
19791
20366
|
}
|
|
@@ -19803,7 +20378,7 @@ function listFiles(cwd) {
|
|
|
19803
20378
|
}
|
|
19804
20379
|
function buildDocsManifest(cwd, files) {
|
|
19805
20380
|
return files.filter((f) => f.startsWith("docs/") && f.endsWith(".md")).map((f) => {
|
|
19806
|
-
const text =
|
|
20381
|
+
const text = readFileSync11(path2.join(cwd, f), "utf8");
|
|
19807
20382
|
const heading = /^#\s+(.+)$/m.exec(text);
|
|
19808
20383
|
return { path: f, title: heading ? heading[1].trim() : path2.basename(f, ".md") };
|
|
19809
20384
|
});
|
|
@@ -19818,7 +20393,7 @@ function buildSurfaceInventory(files) {
|
|
|
19818
20393
|
}
|
|
19819
20394
|
function buildCorpus(options) {
|
|
19820
20395
|
const { cwd } = options;
|
|
19821
|
-
const raw = options.fromJson ? JSON.parse(
|
|
20396
|
+
const raw = options.fromJson ? JSON.parse(readFileSync11(path2.resolve(cwd, options.fromJson), "utf8")) : options.fromGit ? fetchPrsViaGit(cwd) : fetchPrsViaGh(cwd, options.limit);
|
|
19822
20397
|
let prs = normalizePrs(raw);
|
|
19823
20398
|
let sinceDroppedUndated = 0;
|
|
19824
20399
|
if (options.since) {
|
|
@@ -19839,21 +20414,21 @@ function buildCorpus(options) {
|
|
|
19839
20414
|
const docs = buildDocsManifest(cwd, files);
|
|
19840
20415
|
const surfaces = buildSurfaceInventory(files);
|
|
19841
20416
|
ensureDir(at(cwd, CORPUS_DIR));
|
|
19842
|
-
|
|
19843
|
-
|
|
20417
|
+
writeFileSync11(at(cwd, PRS_FILE), prs.map((pr) => JSON.stringify(pr)).join("\n") + (prs.length ? "\n" : ""));
|
|
20418
|
+
writeFileSync11(at(cwd, DOCS_FILE), `${JSON.stringify(docs, null, 2)}
|
|
19844
20419
|
`);
|
|
19845
|
-
|
|
20420
|
+
writeFileSync11(at(cwd, SURFACES_FILE), `${JSON.stringify(surfaces, null, 2)}
|
|
19846
20421
|
`);
|
|
19847
20422
|
return { prs: prs.length, docs: docs.length, surfaces: surfaces.length, sinceDroppedUndated };
|
|
19848
20423
|
}
|
|
19849
20424
|
function readCorpusPrs(cwd) {
|
|
19850
20425
|
const file2 = at(cwd, PRS_FILE);
|
|
19851
|
-
if (!
|
|
19852
|
-
return
|
|
20426
|
+
if (!existsSync11(file2)) return [];
|
|
20427
|
+
return readFileSync11(file2, "utf8").split("\n").filter(Boolean).map((line2) => JSON.parse(line2));
|
|
19853
20428
|
}
|
|
19854
20429
|
|
|
19855
20430
|
// src/lib/bootstrap/fragments.ts
|
|
19856
|
-
import { existsSync as
|
|
20431
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
|
|
19857
20432
|
import path3 from "node:path";
|
|
19858
20433
|
function isArrayOfObjects(value) {
|
|
19859
20434
|
return value === void 0 || Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null && !Array.isArray(v));
|
|
@@ -19879,13 +20454,13 @@ function loadFragments(cwd, manifest) {
|
|
|
19879
20454
|
problems.push({ unit: String(unit2.id), message: err instanceof Error ? err.message : String(err) });
|
|
19880
20455
|
continue;
|
|
19881
20456
|
}
|
|
19882
|
-
if (!
|
|
20457
|
+
if (!existsSync12(file2)) {
|
|
19883
20458
|
missing.push(unit2.id);
|
|
19884
20459
|
continue;
|
|
19885
20460
|
}
|
|
19886
20461
|
let parsed;
|
|
19887
20462
|
try {
|
|
19888
|
-
parsed = JSON.parse(
|
|
20463
|
+
parsed = JSON.parse(readFileSync12(file2, "utf8"));
|
|
19889
20464
|
} catch (err) {
|
|
19890
20465
|
problems.push({ unit: unit2.id, message: `not valid JSON: ${err instanceof Error ? err.message : "parse error"}` });
|
|
19891
20466
|
continue;
|
|
@@ -19922,7 +20497,7 @@ function renderIndex(bundle) {
|
|
|
19922
20497
|
}
|
|
19923
20498
|
|
|
19924
20499
|
// src/lib/bootstrap/manifest.ts
|
|
19925
|
-
import { existsSync as
|
|
20500
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "node:fs";
|
|
19926
20501
|
import path4 from "node:path";
|
|
19927
20502
|
|
|
19928
20503
|
// src/lib/bootstrap/era-window.ts
|
|
@@ -20030,19 +20605,19 @@ function assertValidProfile(profile) {
|
|
|
20030
20605
|
|
|
20031
20606
|
// src/lib/bootstrap/manifest.ts
|
|
20032
20607
|
function readProfile(cwd) {
|
|
20033
|
-
const file2 = at(cwd,
|
|
20034
|
-
if (!
|
|
20608
|
+
const file2 = at(cwd, PROFILE_FILE2);
|
|
20609
|
+
if (!existsSync13(file2)) return null;
|
|
20035
20610
|
try {
|
|
20036
|
-
return JSON.parse(
|
|
20611
|
+
return JSON.parse(readFileSync13(file2, "utf8"));
|
|
20037
20612
|
} catch (err) {
|
|
20038
|
-
throw new Error(`cannot read ${
|
|
20613
|
+
throw new Error(`cannot read ${PROFILE_FILE2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20039
20614
|
}
|
|
20040
20615
|
}
|
|
20041
20616
|
function readManifest(cwd) {
|
|
20042
20617
|
const file2 = at(cwd, MANIFEST_FILE);
|
|
20043
|
-
if (!
|
|
20618
|
+
if (!existsSync13(file2)) return null;
|
|
20044
20619
|
try {
|
|
20045
|
-
return JSON.parse(
|
|
20620
|
+
return JSON.parse(readFileSync13(file2, "utf8"));
|
|
20046
20621
|
} catch (err) {
|
|
20047
20622
|
throw new Error(`cannot read ${MANIFEST_FILE}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20048
20623
|
}
|
|
@@ -20050,15 +20625,15 @@ function readManifest(cwd) {
|
|
|
20050
20625
|
function writeManifest(cwd, manifest) {
|
|
20051
20626
|
ensureDir(at(cwd, PLAN_DIR));
|
|
20052
20627
|
ensureDir(at(cwd, FRAGMENTS_DIR));
|
|
20053
|
-
|
|
20628
|
+
writeFileSync12(at(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
20054
20629
|
`);
|
|
20055
20630
|
}
|
|
20056
20631
|
function detectMode(cwd, bundlePath) {
|
|
20057
20632
|
const file2 = path4.resolve(cwd, bundlePath);
|
|
20058
|
-
if (!
|
|
20633
|
+
if (!existsSync13(file2)) return "greenfield";
|
|
20059
20634
|
let parsed;
|
|
20060
20635
|
try {
|
|
20061
|
-
parsed = JSON.parse(
|
|
20636
|
+
parsed = JSON.parse(readFileSync13(file2, "utf8"));
|
|
20062
20637
|
} catch (err) {
|
|
20063
20638
|
throw new Error(`cannot read bundle at ${bundlePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20064
20639
|
}
|
|
@@ -20511,7 +21086,7 @@ function mergeFragments(input) {
|
|
|
20511
21086
|
}
|
|
20512
21087
|
|
|
20513
21088
|
// src/lib/bootstrap/slice.ts
|
|
20514
|
-
import { existsSync as
|
|
21089
|
+
import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
|
|
20515
21090
|
|
|
20516
21091
|
// src/lib/bootstrap/body-budget.ts
|
|
20517
21092
|
var LAB_NOTE_HEADING_RE = /^##\s+Lab Note.*$/m;
|
|
@@ -20553,8 +21128,8 @@ function boundBody(body) {
|
|
|
20553
21128
|
|
|
20554
21129
|
// src/lib/bootstrap/slice.ts
|
|
20555
21130
|
function readJsonArray(file2) {
|
|
20556
|
-
if (!
|
|
20557
|
-
const parsed = JSON.parse(
|
|
21131
|
+
if (!existsSync14(file2)) return [];
|
|
21132
|
+
const parsed = JSON.parse(readFileSync14(file2, "utf8"));
|
|
20558
21133
|
return Array.isArray(parsed) ? parsed : [];
|
|
20559
21134
|
}
|
|
20560
21135
|
function toPosix(value) {
|
|
@@ -20575,7 +21150,7 @@ function eraWindows(cwd, slugs) {
|
|
|
20575
21150
|
const era = bySlug.get(slug);
|
|
20576
21151
|
if (!era) {
|
|
20577
21152
|
throw new Error(
|
|
20578
|
-
`era "${slug}" is not declared in ${
|
|
21153
|
+
`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.`
|
|
20579
21154
|
);
|
|
20580
21155
|
}
|
|
20581
21156
|
assertEraWindow(era);
|
|
@@ -20664,7 +21239,7 @@ ${usage}`);
|
|
|
20664
21239
|
}
|
|
20665
21240
|
function writeFileAtomic(filePath, content) {
|
|
20666
21241
|
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
20667
|
-
|
|
21242
|
+
writeFileSync13(tmpPath, content);
|
|
20668
21243
|
renameSync(tmpPath, filePath);
|
|
20669
21244
|
}
|
|
20670
21245
|
function runCorpus(argv) {
|
|
@@ -20699,7 +21274,7 @@ ${CORPUS_USAGE}`);
|
|
|
20699
21274
|
${CORPUS_USAGE}`);
|
|
20700
21275
|
}
|
|
20701
21276
|
}
|
|
20702
|
-
if (!
|
|
21277
|
+
if (!existsSync15(path5.join(cwd, ".git"))) {
|
|
20703
21278
|
fail11("`arkaik bootstrap corpus` must run from the repository root (no .git here).");
|
|
20704
21279
|
}
|
|
20705
21280
|
try {
|
|
@@ -20761,7 +21336,7 @@ ${PLAN_USAGE}`);
|
|
|
20761
21336
|
|
|
20762
21337
|
${PLAN_USAGE}`);
|
|
20763
21338
|
}
|
|
20764
|
-
if (!
|
|
21339
|
+
if (!existsSync15(path5.join(cwd, ".git"))) {
|
|
20765
21340
|
fail11("`arkaik bootstrap plan` must run from the repository root (no .git here).");
|
|
20766
21341
|
}
|
|
20767
21342
|
try {
|
|
@@ -20900,7 +21475,7 @@ ${MERGE_USAGE}`);
|
|
|
20900
21475
|
}
|
|
20901
21476
|
try {
|
|
20902
21477
|
const bundlePath = path5.resolve(cwd, manifest.bundle);
|
|
20903
|
-
const base =
|
|
21478
|
+
const base = existsSync15(bundlePath) ? readBundle(bundlePath) : {
|
|
20904
21479
|
schema_version: 3,
|
|
20905
21480
|
project: {
|
|
20906
21481
|
id: path5.basename(cwd),
|
|
@@ -20936,7 +21511,7 @@ ${MERGE_USAGE}`);
|
|
|
20936
21511
|
const journalPath = journalPathFor(bundlePath);
|
|
20937
21512
|
const journalText = result.journal.map((e) => JSON.stringify(e)).join("\n") + (result.journal.length ? "\n" : "");
|
|
20938
21513
|
if (!dryRun) {
|
|
20939
|
-
|
|
21514
|
+
mkdirSync8(path5.dirname(bundlePath), { recursive: true });
|
|
20940
21515
|
writeFileAtomic(bundlePath, serialized);
|
|
20941
21516
|
writeFileAtomic(journalPath, journalText);
|
|
20942
21517
|
}
|
|
@@ -20985,138 +21560,6 @@ ${USAGE12}`);
|
|
|
20985
21560
|
}
|
|
20986
21561
|
}
|
|
20987
21562
|
|
|
20988
|
-
// ../schema/src/cli/kritik-audit.ts
|
|
20989
|
-
import { existsSync as existsSync14, readdirSync as readdirSync3, statSync } from "node:fs";
|
|
20990
|
-
import { join as join7 } from "node:path";
|
|
20991
|
-
|
|
20992
|
-
// ../schema/src/cli/kritik-paths.ts
|
|
20993
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync13 } from "node:fs";
|
|
20994
|
-
import { dirname as dirname5, join as join6, resolve as resolve8 } from "node:path";
|
|
20995
|
-
var QUALITY_DIR = "docs/quality";
|
|
20996
|
-
var PROFILE_FILE2 = "profile.json";
|
|
20997
|
-
var OVERLAY_FILE = "criteria.custom.json";
|
|
20998
|
-
var AUDITS_DIR = "audits";
|
|
20999
|
-
var PACK_FILE = "library.json";
|
|
21000
|
-
function readJson(path6) {
|
|
21001
|
-
return JSON.parse(readFileSync14(path6, "utf8"));
|
|
21002
|
-
}
|
|
21003
|
-
function writeJson(path6, value) {
|
|
21004
|
-
mkdirSync8(dirname5(path6), { recursive: true });
|
|
21005
|
-
writeFileSync13(path6, JSON.stringify(value, null, 2) + "\n");
|
|
21006
|
-
}
|
|
21007
|
-
var profilePath = (root) => join6(root, QUALITY_DIR, PROFILE_FILE2);
|
|
21008
|
-
var overlayPath = (root) => join6(root, QUALITY_DIR, OVERLAY_FILE);
|
|
21009
|
-
var auditDir = (root, auditId) => join6(root, QUALITY_DIR, AUDITS_DIR, auditId);
|
|
21010
|
-
function loadProfile(root) {
|
|
21011
|
-
const path6 = profilePath(root);
|
|
21012
|
-
return existsSync13(path6) ? readJson(path6) : null;
|
|
21013
|
-
}
|
|
21014
|
-
function loadOverlay(root) {
|
|
21015
|
-
const path6 = overlayPath(root);
|
|
21016
|
-
return existsSync13(path6) ? readJson(path6) : null;
|
|
21017
|
-
}
|
|
21018
|
-
|
|
21019
|
-
// ../schema/src/cli/kritik-audit.ts
|
|
21020
|
-
var SCORES_FILE = "scores.json";
|
|
21021
|
-
var FINDINGS_FILE = "findings.json";
|
|
21022
|
-
var MATRIX_FILE = "matrix.json";
|
|
21023
|
-
var scoresPath = (root, auditId) => join7(auditDir(root, auditId), SCORES_FILE);
|
|
21024
|
-
var findingsPath = (root, auditId) => join7(auditDir(root, auditId), FINDINGS_FILE);
|
|
21025
|
-
var matrixPath = (root, auditId) => join7(auditDir(root, auditId), MATRIX_FILE);
|
|
21026
|
-
function listAuditIds(root) {
|
|
21027
|
-
const dir = join7(root, QUALITY_DIR, AUDITS_DIR);
|
|
21028
|
-
if (!existsSync14(dir)) return [];
|
|
21029
|
-
return readdirSync3(dir).filter((name) => statSync(join7(dir, name)).isDirectory()).sort();
|
|
21030
|
-
}
|
|
21031
|
-
function newestAuditId(root) {
|
|
21032
|
-
const dir = join7(root, QUALITY_DIR, AUDITS_DIR);
|
|
21033
|
-
if (!existsSync14(dir)) throw new Error(`no audits directory at ${dir}`);
|
|
21034
|
-
const ids = listAuditIds(root);
|
|
21035
|
-
if (ids.length === 0) throw new Error(`no audits found under ${dir}`);
|
|
21036
|
-
return ids[ids.length - 1];
|
|
21037
|
-
}
|
|
21038
|
-
function loadScores(root, auditId) {
|
|
21039
|
-
const path6 = scoresPath(root, auditId);
|
|
21040
|
-
if (!existsSync14(path6)) throw new Error(`no ${SCORES_FILE} at ${path6}`);
|
|
21041
|
-
const file2 = readJson(path6);
|
|
21042
|
-
return { ...file2, assessments: Array.isArray(file2.assessments) ? file2.assessments : [] };
|
|
21043
|
-
}
|
|
21044
|
-
function loadScoresOrEmpty(root, auditId) {
|
|
21045
|
-
return existsSync14(scoresPath(root, auditId)) ? loadScores(root, auditId) : { audit_id: auditId, assessments: [] };
|
|
21046
|
-
}
|
|
21047
|
-
function loadFindings(root, auditId) {
|
|
21048
|
-
const path6 = findingsPath(root, auditId);
|
|
21049
|
-
if (!existsSync14(path6)) return { audit_id: auditId, findings: [] };
|
|
21050
|
-
const file2 = readJson(path6);
|
|
21051
|
-
return { ...file2, findings: Array.isArray(file2.findings) ? file2.findings : [] };
|
|
21052
|
-
}
|
|
21053
|
-
function saveScores(root, auditId, file2) {
|
|
21054
|
-
writeJson(scoresPath(root, auditId), file2);
|
|
21055
|
-
}
|
|
21056
|
-
function saveFindings(root, auditId, file2) {
|
|
21057
|
-
writeJson(findingsPath(root, auditId), file2);
|
|
21058
|
-
}
|
|
21059
|
-
function requireProfile(root) {
|
|
21060
|
-
const profile = loadProfile(root);
|
|
21061
|
-
if (!profile) {
|
|
21062
|
-
throw new Error(
|
|
21063
|
-
`no profile at ${join7(root, QUALITY_DIR, "profile.json")} \u2014 pick this project's surfaces first (\`arkaik kritik profile\`, or the plugin's init-profile.js).`
|
|
21064
|
-
);
|
|
21065
|
-
}
|
|
21066
|
-
return profile;
|
|
21067
|
-
}
|
|
21068
|
-
function loadQualitySection(root, auditId, library, scores = loadScores(root, auditId)) {
|
|
21069
|
-
const findings = loadFindings(root, auditId);
|
|
21070
|
-
return {
|
|
21071
|
-
framework_version: scores.framework_version ?? library.version,
|
|
21072
|
-
profile: requireProfile(root),
|
|
21073
|
-
assessments: scores.assessments,
|
|
21074
|
-
findings: findings.findings
|
|
21075
|
-
};
|
|
21076
|
-
}
|
|
21077
|
-
function computeAuditMatrix(root, auditId, library) {
|
|
21078
|
-
const scores = loadScores(root, auditId);
|
|
21079
|
-
const section = loadQualitySection(root, auditId, library, scores);
|
|
21080
|
-
const matrix = deriveQualityMatrix({ quality: section }, library);
|
|
21081
|
-
const file2 = {
|
|
21082
|
-
audit_id: auditId,
|
|
21083
|
-
commit: scores.commit,
|
|
21084
|
-
framework_version: section.framework_version,
|
|
21085
|
-
matrix: matrix.matrix,
|
|
21086
|
-
overall: matrix.overall,
|
|
21087
|
-
finding_counts: matrix.finding_counts
|
|
21088
|
-
};
|
|
21089
|
-
writeJson(matrixPath(root, auditId), file2);
|
|
21090
|
-
return { section, matrix, file: file2 };
|
|
21091
|
-
}
|
|
21092
|
-
function renderMatrixMarkdown(matrix, domainNames) {
|
|
21093
|
-
const cell = (value) => value ? `${value.score} (${value.grade}${value.capped ? "*" : ""})` : "\u2014";
|
|
21094
|
-
const lines = [];
|
|
21095
|
-
lines.push(`| Domain | ${matrix.surfaces.join(" | ")} |`);
|
|
21096
|
-
lines.push(`| --- | ${matrix.surfaces.map(() => "---").join(" | ")} |`);
|
|
21097
|
-
for (const domain2 of matrix.domains) {
|
|
21098
|
-
const label = domainNames.get(domain2) ?? domain2;
|
|
21099
|
-
lines.push(
|
|
21100
|
-
`| **${domain2}** ${label} | ${matrix.surfaces.map((s) => cell(matrix.matrix[domain2]?.[s])).join(" | ")} |`
|
|
21101
|
-
);
|
|
21102
|
-
}
|
|
21103
|
-
lines.push(
|
|
21104
|
-
`| **Overall (weighted)** | ${matrix.surfaces.map((s) => {
|
|
21105
|
-
const score = matrix.overall[s];
|
|
21106
|
-
return score === null || score === void 0 ? "\u2014" : `**${score} (${gradeOf(score)})**`;
|
|
21107
|
-
}).join(" | ")} |`
|
|
21108
|
-
);
|
|
21109
|
-
return lines.join("\n");
|
|
21110
|
-
}
|
|
21111
|
-
function locateFinding(root, id) {
|
|
21112
|
-
for (const auditId of [...listAuditIds(root)].reverse()) {
|
|
21113
|
-
const file2 = loadFindings(root, auditId);
|
|
21114
|
-
const finding = file2.findings.find((candidate) => candidate.id === id);
|
|
21115
|
-
if (finding) return { auditId, file: file2, finding };
|
|
21116
|
-
}
|
|
21117
|
-
return void 0;
|
|
21118
|
-
}
|
|
21119
|
-
|
|
21120
21563
|
// ../schema/src/cli/kritik-overlay.ts
|
|
21121
21564
|
var ANCHOR_KEYS = ["l0", "l1", "l2", "l3", "l4"];
|
|
21122
21565
|
var CRITERION_TEMPLATE = {
|
|
@@ -21247,49 +21690,6 @@ Pass --domain-name "<display name>" to define it, or use an existing domain code
|
|
|
21247
21690
|
|
|
21248
21691
|
// src/commands/kritik.ts
|
|
21249
21692
|
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
|
|
21250
|
-
|
|
21251
|
-
// src/lib/kritik-io.ts
|
|
21252
|
-
import { existsSync as existsSync15 } from "node:fs";
|
|
21253
|
-
import { dirname as dirname6, join as join8 } from "node:path";
|
|
21254
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
21255
|
-
var KRITIK_ACTOR = "arkaik-cli";
|
|
21256
|
-
var DEFAULT_BUNDLE_PATH9 = join8("docs", "arkaik", "bundle.json");
|
|
21257
|
-
var VENDORED_PACK = join8(QUALITY_DIR, PACK_FILE);
|
|
21258
|
-
var BUNDLED_PACK = join8(dirname6(fileURLToPath2(import.meta.url)), "assets", "kritik", "library.json");
|
|
21259
|
-
function resolvePack(root) {
|
|
21260
|
-
const vendored = join8(root, VENDORED_PACK);
|
|
21261
|
-
if (existsSync15(vendored)) return { library: readJson(vendored), path: vendored, vendored: true };
|
|
21262
|
-
if (!existsSync15(BUNDLED_PACK)) {
|
|
21263
|
-
throw new Error(
|
|
21264
|
-
`no criteria pack found. Looked in:
|
|
21265
|
-
${vendored}
|
|
21266
|
-
${BUNDLED_PACK}
|
|
21267
|
-
The second is shipped with this CLI, so its absence means a broken install \u2014 reinstall \`arkaik\`.`
|
|
21268
|
-
);
|
|
21269
|
-
}
|
|
21270
|
-
return { library: readJson(BUNDLED_PACK), path: BUNDLED_PACK, vendored: false };
|
|
21271
|
-
}
|
|
21272
|
-
function loadKritikLibrary(root) {
|
|
21273
|
-
const pack = resolvePack(root);
|
|
21274
|
-
return { library: mergeKritikLibrary(pack.library, loadOverlay(root)), pack };
|
|
21275
|
-
}
|
|
21276
|
-
function resolveJournal(root, bundlePath) {
|
|
21277
|
-
const resolved = bundlePath ?? join8(root, DEFAULT_BUNDLE_PATH9);
|
|
21278
|
-
return { bundlePath: resolved, journalPath: journalPathFor(resolved), present: existsSync15(resolved) };
|
|
21279
|
-
}
|
|
21280
|
-
function appendQualityEvents(root, inputs, options = {}) {
|
|
21281
|
-
if (inputs.length === 0) return { events: [] };
|
|
21282
|
-
const actor = options.actor ?? KRITIK_ACTOR;
|
|
21283
|
-
const journal = resolveJournal(root, options.bundlePath);
|
|
21284
|
-
if (!journal.present) return { events: [] };
|
|
21285
|
-
const bundle = readBundle(journal.bundlePath);
|
|
21286
|
-
const baseline = ensureJournalBaseline(journal.journalPath, bundle, actor);
|
|
21287
|
-
const events = inputs.map((input) => makeEvent(input.type, input.payload, { actor }));
|
|
21288
|
-
for (const event of events) appendJournalEvent(journal.journalPath, event);
|
|
21289
|
-
return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
|
|
21290
|
-
}
|
|
21291
|
-
|
|
21292
|
-
// src/commands/kritik.ts
|
|
21293
21693
|
var USAGE13 = `arkaik kritik <subcommand> [options]
|
|
21294
21694
|
|
|
21295
21695
|
Audit this product's quality with the Kritik framework: a maturity level per
|
|
@@ -21304,6 +21704,7 @@ Subcommands:
|
|
|
21304
21704
|
finding accept <id> Accept it as a known, owned risk.
|
|
21305
21705
|
matrix [audit-id] Roll an audit up (writes matrix.json).
|
|
21306
21706
|
signals The signal pack, and what has tripped since the last audit.
|
|
21707
|
+
regressions What got worse between two audits.
|
|
21307
21708
|
issue <criterion> Print the prefilled GitHub issue skeleton.
|
|
21308
21709
|
criterion add ... Add a project-specific criterion to the overlay.
|
|
21309
21710
|
|
|
@@ -21377,6 +21778,20 @@ makes it usable as a CI step.
|
|
|
21377
21778
|
--trip Record one as tripped: appends quality.signal.tripped.
|
|
21378
21779
|
--signal takes the row's index from the run sheet, or the text.
|
|
21379
21780
|
--json The full run sheet as JSON (what an agent should read).`;
|
|
21781
|
+
var REGRESSIONS_USAGE = `arkaik kritik regressions [--from <audit>] [--to <audit>] [--record] [--json]
|
|
21782
|
+
|
|
21783
|
+
What got worse between two audits: a cell whose maturity dropped, a cell that
|
|
21784
|
+
gained an open Critical or High finding, a finding that was resolved and is open
|
|
21785
|
+
again. Cells scored in only one of the two audits are not compared \u2014 a
|
|
21786
|
+
half-finished audit is not a regression.
|
|
21787
|
+
|
|
21788
|
+
Exits 1 when anything regressed, which is what makes it usable as a CI step or a
|
|
21789
|
+
scheduled routine.
|
|
21790
|
+
|
|
21791
|
+
--from <audit> The older reading (default: the audit before --to).
|
|
21792
|
+
--to <audit> The newer reading (default: the newest on disk).
|
|
21793
|
+
--record Append one quality.signal.tripped per regression.
|
|
21794
|
+
--json The full list as JSON.`;
|
|
21380
21795
|
var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
|
|
21381
21796
|
|
|
21382
21797
|
Print the criterion's GitHub issue skeleton, filled as far as what we know
|
|
@@ -21930,7 +22345,8 @@ function runSignals(args, common) {
|
|
|
21930
22345
|
console.log(
|
|
21931
22346
|
`
|
|
21932
22347
|
${rows.length} checks across ${criteria} criteria and ${(profile.surfaces ?? []).length} surfaces.
|
|
21933
|
-
Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.`
|
|
22348
|
+
Narrow it (--surface, --criterion, --domain) to read them, or --json to take the lot.` + (listAuditIds(common.root).length > 1 ? `
|
|
22349
|
+
Comparing two audits is \`arkaik kritik regressions\`.` : "")
|
|
21934
22350
|
);
|
|
21935
22351
|
}
|
|
21936
22352
|
if (trips.length > 0) {
|
|
@@ -21946,6 +22362,83 @@ function runSignals(args, common) {
|
|
|
21946
22362
|
console.log("");
|
|
21947
22363
|
process.exit(0);
|
|
21948
22364
|
}
|
|
22365
|
+
function auditPair(root, from, to) {
|
|
22366
|
+
const audits = listAuditIds(root);
|
|
22367
|
+
if (audits.length < 2) {
|
|
22368
|
+
fail12(
|
|
22369
|
+
`kritik: regressions needs two audits to compare \u2014 ${audits.length === 0 ? "docs/quality/audits/ holds none" : `only "${audits[0]}" exists`}.
|
|
22370
|
+
A regression is the difference between two readings; one reading is a baseline.`
|
|
22371
|
+
);
|
|
22372
|
+
}
|
|
22373
|
+
const known = (id) => {
|
|
22374
|
+
if (!audits.includes(id)) fail12(`kritik: no audit "${id}" under docs/quality/audits/ (have: ${audits.join(", ")})`);
|
|
22375
|
+
return id;
|
|
22376
|
+
};
|
|
22377
|
+
const newer = to === void 0 ? audits[audits.length - 1] : known(to);
|
|
22378
|
+
const older = from === void 0 ? audits[audits.indexOf(newer) - 1] : known(from);
|
|
22379
|
+
if (older === void 0) {
|
|
22380
|
+
fail12(`kritik: "${newer}" is the oldest audit \u2014 there is nothing before it to compare against.`);
|
|
22381
|
+
}
|
|
22382
|
+
if (older === newer) {
|
|
22383
|
+
fail12(`kritik: --from and --to name the same audit ("${newer}") \u2014 a regression needs two readings.`);
|
|
22384
|
+
}
|
|
22385
|
+
if (audits.indexOf(older) > audits.indexOf(newer)) {
|
|
22386
|
+
fail12(`kritik: --from "${older}" is newer than --to "${newer}" \u2014 swap them, or the comparison inverts.`);
|
|
22387
|
+
}
|
|
22388
|
+
return { from: older, to: newer };
|
|
22389
|
+
}
|
|
22390
|
+
function runRegressions(args, common) {
|
|
22391
|
+
const { single, flags } = collect(args, [], ["json", "record"]);
|
|
22392
|
+
if (flags.has("help")) {
|
|
22393
|
+
console.log(REGRESSIONS_USAGE);
|
|
22394
|
+
process.exit(0);
|
|
22395
|
+
}
|
|
22396
|
+
const library = loadLibraryOrFail(common.root);
|
|
22397
|
+
profileOrFail(common.root);
|
|
22398
|
+
const { from, to } = auditPair(common.root, single.from, single.to);
|
|
22399
|
+
let regressions;
|
|
22400
|
+
try {
|
|
22401
|
+
regressions = detectRegressions(
|
|
22402
|
+
loadQualitySection(common.root, from, library),
|
|
22403
|
+
loadQualitySection(common.root, to, library),
|
|
22404
|
+
library
|
|
22405
|
+
);
|
|
22406
|
+
} catch (error51) {
|
|
22407
|
+
return fail12(`kritik: ${error51.message}`);
|
|
22408
|
+
}
|
|
22409
|
+
if (flags.has("json")) {
|
|
22410
|
+
console.log(JSON.stringify({ from, to, total: regressions.length, regressions }, null, 2));
|
|
22411
|
+
} else if (regressions.length === 0) {
|
|
22412
|
+
console.log(`
|
|
22413
|
+
nothing regressed between ${from} and ${to}.
|
|
22414
|
+
`);
|
|
22415
|
+
} else {
|
|
22416
|
+
console.log("");
|
|
22417
|
+
for (const regression of regressions) {
|
|
22418
|
+
console.log(` [${regression.kind}] ${regression.criterion_id} x ${regression.surface}`);
|
|
22419
|
+
console.log(` ${regression.detail}`);
|
|
22420
|
+
}
|
|
22421
|
+
console.log(`
|
|
22422
|
+
${regressions.length} regression${regressions.length === 1 ? "" : "s"} between ${from} and ${to}.`);
|
|
22423
|
+
}
|
|
22424
|
+
if (flags.has("record") && regressions.length > 0) {
|
|
22425
|
+
reportJournal(
|
|
22426
|
+
common.root,
|
|
22427
|
+
regressions.map(
|
|
22428
|
+
(regression) => signalTrippedInput({
|
|
22429
|
+
criterion_id: regression.criterion_id,
|
|
22430
|
+
surface: regression.surface,
|
|
22431
|
+
signal: regression.signal,
|
|
22432
|
+
detail: regression.detail
|
|
22433
|
+
})
|
|
22434
|
+
),
|
|
22435
|
+
common
|
|
22436
|
+
);
|
|
22437
|
+
console.log(` a tripped signal is not a finding \u2014 it is the prompt to go look.
|
|
22438
|
+
`);
|
|
22439
|
+
}
|
|
22440
|
+
process.exit(regressions.length > 0 ? 1 : 0);
|
|
22441
|
+
}
|
|
21949
22442
|
function runIssue(args, common) {
|
|
21950
22443
|
const { single, flags, positionals } = collect(args);
|
|
21951
22444
|
if (flags.has("help")) {
|
|
@@ -22085,6 +22578,8 @@ function runKritik(args) {
|
|
|
22085
22578
|
return runMatrix(subArgs, common);
|
|
22086
22579
|
case "signals":
|
|
22087
22580
|
return runSignals(subArgs, common);
|
|
22581
|
+
case "regressions":
|
|
22582
|
+
return runRegressions(subArgs, common);
|
|
22088
22583
|
case "issue":
|
|
22089
22584
|
return runIssue(subArgs, common);
|
|
22090
22585
|
case "criterion":
|
|
@@ -22109,13 +22604,13 @@ Commands:
|
|
|
22109
22604
|
release <version> [path] Tag a release (append release.tagged) and draft its notes.
|
|
22110
22605
|
deliverable <title> [path] Record a deliverable (append deliverable.shipped).
|
|
22111
22606
|
sync [options] [path] Mirror external ref status (GitHub issues/PRs) into node refs.
|
|
22112
|
-
pack [options] [path] Produce a
|
|
22607
|
+
pack [options] [path] Produce a self-contained interchange bundle (embeds journal + quality).
|
|
22113
22608
|
open [options] [path] Validate, then hand off the packed bundle to arkaik.app import.
|
|
22114
|
-
push [options] [path] Validate, pack (journal stripped), and publish to Publik.
|
|
22609
|
+
push [options] [path] Validate, pack (journal + quality stripped), and publish to Publik.
|
|
22115
22610
|
--delete <id> --key <owner_key> removes a snapshot.
|
|
22116
22611
|
link [options] [path] Point this repo at a hosted project so an agent can edit it.
|
|
22117
22612
|
--list shows the projects your token can reach.
|
|
22118
|
-
restore [options] [path] Replace the linked
|
|
22613
|
+
restore [options] [path] Replace the linked project's bundle, journal + quality (backs up first).
|
|
22119
22614
|
bootstrap <sub> [options] One-time onboarding: mine, plan, slice, merge a map from a repo.
|
|
22120
22615
|
kritik <sub> [options] Quality audits: score criteria, open findings, roll up the matrix.
|
|
22121
22616
|
|
|
@@ -22124,7 +22619,7 @@ Options:
|
|
|
22124
22619
|
-v, --version Print the version.
|
|
22125
22620
|
|
|
22126
22621
|
Run "arkaik <command> --help" for command-specific help.`;
|
|
22127
|
-
var VERSION = "0.
|
|
22622
|
+
var VERSION = "0.4.0";
|
|
22128
22623
|
function main(argv) {
|
|
22129
22624
|
const [command, ...rest] = argv;
|
|
22130
22625
|
if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
|