macca-method 2.1.2 → 3.0.0-rc.1
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/.agents/macca-lock.json +2 -1
- package/.agents/macca-managed-skills.txt +1 -0
- package/.agents/skills/_shared/references/additional-skills.md +17 -1
- package/.agents/skills/_shared/references/brainstorm-session.md +61 -21
- package/.agents/skills/_shared/references/config-mutation.md +58 -18
- package/.agents/skills/_shared/references/fix-mode.md +21 -7
- package/.agents/skills/_shared/references/human-loop.md +2 -0
- package/.agents/skills/_shared/references/implementation-principles.md +4 -0
- package/.agents/skills/_shared/references/interaction-contract.md +33 -0
- package/.agents/skills/_shared/references/invocation-policy.md +14 -3
- package/.agents/skills/_shared/references/language-config.md +24 -4
- package/.agents/skills/_shared/references/output-ownership.md +12 -5
- package/.agents/skills/_shared/references/planning-principles.md +35 -0
- package/.agents/skills/_shared/references/scope-delta.md +21 -0
- package/.agents/skills/_shared/references/scope-rules.md +1 -1
- package/.agents/skills/_shared/references/skill-catalog.md +30 -13
- package/.agents/skills/_shared/references/workspace-safety.md +11 -0
- package/.agents/skills/_shared/scripts/config-file.js +132 -0
- package/.agents/skills/_shared/scripts/config-validator.js +164 -0
- package/.agents/skills/_shared/scripts/read-preferences.js +128 -0
- package/.agents/skills/add-feature/SKILL.md +24 -18
- package/.agents/skills/brainstorm-api/SKILL.md +24 -23
- package/.agents/skills/brainstorm-api/assets/api.template.md +11 -23
- package/.agents/skills/brainstorm-architecture/SKILL.md +41 -48
- package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +24 -16
- package/.agents/skills/brainstorm-prd/SKILL.md +26 -28
- package/.agents/skills/brainstorm-prd/assets/PRD.template.md +10 -9
- package/.agents/skills/brainstorm-rules/SKILL.md +42 -46
- package/.agents/skills/brainstorm-rules/assets/rules.template.md +8 -8
- package/.agents/skills/brainstorm-schema/SKILL.md +17 -17
- package/.agents/skills/brainstorm-schema/assets/schema.template.md +9 -8
- package/.agents/skills/brainstorm-styleguide/SKILL.md +27 -26
- package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +27 -51
- package/.agents/skills/brainstorm-task/SKILL.md +53 -37
- package/.agents/skills/brainstorm-task/assets/Task.template.md +11 -12
- package/.agents/skills/bug-fix/SKILL.md +78 -175
- package/.agents/skills/bug-fix/assets/bug-log.template.md +61 -0
- package/.agents/skills/code-review/SKILL.md +14 -6
- package/.agents/skills/code-review/references/review-checklist.md +24 -44
- package/.agents/skills/developer/SKILL.md +23 -25
- package/.agents/skills/developer/references/close-phase.md +11 -6
- package/.agents/skills/developer/references/execute-task.md +15 -8
- package/.agents/skills/developer/references/onboarding.md +16 -19
- package/.agents/skills/help/SKILL.md +49 -49
- package/.agents/skills/meet/SKILL.md +37 -85
- package/.agents/skills/quick-dev/SKILL.md +40 -75
- package/.agents/skills/release-readiness/SKILL.md +1 -1
- package/.agents/skills/setup-macca-method/SKILL.md +79 -0
- package/.agents/skills/spec-audit/SKILL.md +21 -17
- package/.agents/skills/spec-compliance/SKILL.md +36 -18
- package/.agents/skills/spec-init/SKILL.md +53 -22
- package/CHANGELOG.md +31 -0
- package/README.md +92 -1195
- package/bin/macca-method.js +833 -286
- package/docs/configuration.md +174 -0
- package/docs/troubleshooting.md +180 -0
- package/docs/workflows.md +175 -0
- package/package.json +48 -4
- package/scripts/lib/npm-command.js +76 -0
- package/scripts/test-cli-setup.js +869 -0
- package/scripts/test-config.js +320 -0
- package/scripts/test-install.js +63 -28
- package/scripts/test-installer-safety.js +545 -0
- package/scripts/test-npm-command.js +143 -0
- package/scripts/test-package-safety.js +252 -0
- package/scripts/test-preferences.js +475 -0
- package/scripts/test-upgrade-legacy.js +11 -21
- package/scripts/validate-docs.js +166 -0
- package/scripts/validate-package.js +285 -0
- package/scripts/validate-skill-behavior.js +914 -12
- package/.agents/skills/antislop-copywriting/SKILL.md +0 -372
- package/.agents/skills/skill-creator/LICENSE.txt +0 -202
- package/.agents/skills/skill-creator/SKILL.md +0 -485
- package/.agents/skills/skill-creator/agents/analyzer.md +0 -274
- package/.agents/skills/skill-creator/agents/comparator.md +0 -202
- package/.agents/skills/skill-creator/agents/grader.md +0 -223
- package/.agents/skills/skill-creator/assets/eval_review.html +0 -146
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.agents/skills/skill-creator/references/schemas.md +0 -441
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
- package/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
- package/.agents/skills/skill-creator/scripts/utils.py +0 -47
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const documentationFiles = [
|
|
9
|
+
"README.md", "docs/workflows.md", "docs/configuration.md", "docs/troubleshooting.md",
|
|
10
|
+
];
|
|
11
|
+
const blank = (text) => text.replace(/[^\r\n]/g, " ");
|
|
12
|
+
|
|
13
|
+
// Preserve offsets so section extraction and diagnostics refer to source text.
|
|
14
|
+
function stripCode(source, inline = true) {
|
|
15
|
+
let fence = null;
|
|
16
|
+
let text = source.split(/(?<=\n)/).map((line) => {
|
|
17
|
+
const marker = line.match(/^ {0,3}(`{3,}|~{3,})([^\r\n]*)/);
|
|
18
|
+
if (fence) {
|
|
19
|
+
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = null;
|
|
20
|
+
return blank(line);
|
|
21
|
+
}
|
|
22
|
+
if (marker && !(marker[1][0] === "`" && marker[2].includes("`"))) {
|
|
23
|
+
fence = marker[1];
|
|
24
|
+
return blank(line);
|
|
25
|
+
}
|
|
26
|
+
return /^(?: {4}|\t)/.test(line) ? blank(line) : line;
|
|
27
|
+
}).join("").replace(/<!--[\s\S]*?-->/g, blank);
|
|
28
|
+
if (inline) text = text.replace(/(`+)(?!`)([\s\S]*?[^`])\1(?!`)/g, blank);
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function destinationAt(text, start) {
|
|
33
|
+
let index = start;
|
|
34
|
+
while (/\s/.test(text[index] || "") && index < text.length) index++;
|
|
35
|
+
if (text[index] === "<") {
|
|
36
|
+
const end = text.indexOf(">", index + 1);
|
|
37
|
+
if (end >= 0) return { destination: text.slice(index + 1, end), end: end + 1 };
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const begin = index;
|
|
41
|
+
let depth = 0;
|
|
42
|
+
for (; index < text.length; index++) {
|
|
43
|
+
const character = text[index];
|
|
44
|
+
if (character === "\\") { index++; continue; }
|
|
45
|
+
if (character === "(") depth++;
|
|
46
|
+
if (character === ")") {
|
|
47
|
+
if (!depth) break;
|
|
48
|
+
depth--;
|
|
49
|
+
}
|
|
50
|
+
if (/\s/.test(character) && !depth) break;
|
|
51
|
+
}
|
|
52
|
+
return depth ? null : { destination: text.slice(begin, index), end: index };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function markdownLinks(source) {
|
|
56
|
+
let text = stripCode(source);
|
|
57
|
+
const definitions = new Map();
|
|
58
|
+
const normalize = (label) => label.trim().replace(/\s+/g, " ").toLowerCase();
|
|
59
|
+
text = text.replace(/^ {0,3}\[([^\]\n]+)\]:[ \t]*(.+)$/gm, (line, label, value) => {
|
|
60
|
+
const parsed = destinationAt(value, 0);
|
|
61
|
+
if (parsed && !definitions.has(normalize(label))) definitions.set(normalize(label), parsed.destination);
|
|
62
|
+
return blank(line);
|
|
63
|
+
});
|
|
64
|
+
const links = [];
|
|
65
|
+
// Inline links and images share the same destination syntax. Backticked
|
|
66
|
+
// filenames and example commands are not links and have already been masked.
|
|
67
|
+
let consumed = 0;
|
|
68
|
+
for (const match of text.matchAll(/(?<!\\)\[([^\]\n]*)\]/g)) {
|
|
69
|
+
if (match.index < consumed) continue;
|
|
70
|
+
const end = match.index + match[0].length;
|
|
71
|
+
let destination;
|
|
72
|
+
if (text[end] === "(") {
|
|
73
|
+
const parsed = destinationAt(text, end + 1);
|
|
74
|
+
if (parsed && /^(?:\s+(?:"[^"\n]*"|'[^'\n]*'|\([^\n]*?\)))?\s*\)/.test(text.slice(parsed.end))) {
|
|
75
|
+
destination = parsed.destination;
|
|
76
|
+
}
|
|
77
|
+
} else if (text[end] === "[") {
|
|
78
|
+
const reference = text.slice(end).match(/^\[([^\]\n]*)\]/);
|
|
79
|
+
if (reference) {
|
|
80
|
+
destination = definitions.get(normalize(reference[1] || match[1]));
|
|
81
|
+
consumed = end + reference[0].length;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
destination = definitions.get(normalize(match[1]));
|
|
85
|
+
}
|
|
86
|
+
if (destination !== undefined) links.push({ destination, offset: match.index });
|
|
87
|
+
}
|
|
88
|
+
return links;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function headingAnchors(source) {
|
|
92
|
+
const anchors = new Set();
|
|
93
|
+
const text = stripCode(source, false);
|
|
94
|
+
const lines = text.split(/\r?\n/);
|
|
95
|
+
for (let index = 0; index < lines.length; index++) {
|
|
96
|
+
const atx = lines[index].match(/^ {0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/);
|
|
97
|
+
const setext = index + 1 < lines.length && /^ {0,3}(?:=+|-+)\s*$/.test(lines[index + 1]) && lines[index].trim();
|
|
98
|
+
if (!atx && !setext) continue;
|
|
99
|
+
const heading = (atx ? atx[1] : lines[index++])
|
|
100
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
101
|
+
.replace(/<[^>]*>/g, "")
|
|
102
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
103
|
+
.replace(/"/g, '"').replace(/'|'/g, "'")
|
|
104
|
+
.replace(/&#(x[\da-f]+|\d+);/gi, (entity, number) => {
|
|
105
|
+
const code = number[0].toLowerCase() === "x" ? parseInt(number.slice(1), 16) : Number(number);
|
|
106
|
+
return code <= 0x10ffff ? String.fromCodePoint(code) : entity;
|
|
107
|
+
});
|
|
108
|
+
// GitHub keeps hyphens/underscores, removes punctuation (not replaces it
|
|
109
|
+
// with a hyphen), and retains repeated spaces as repeated hyphens.
|
|
110
|
+
const base = heading.toLowerCase().replace(/[^\p{L}\p{M}\p{N}\p{Pc}\- ]/gu, "").replace(/ /g, "-");
|
|
111
|
+
let slug = base;
|
|
112
|
+
for (let suffix = 1; anchors.has(slug); suffix++) slug = `${base}-${suffix}`;
|
|
113
|
+
anchors.add(slug);
|
|
114
|
+
}
|
|
115
|
+
return anchors;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateDocs(root, packagedFiles) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
const anchors = new Map();
|
|
121
|
+
for (const file of documentationFiles) {
|
|
122
|
+
const source = fs.readFileSync(path.join(root, file), "utf8");
|
|
123
|
+
for (const { destination, offset } of markdownLinks(source)) {
|
|
124
|
+
if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(destination)) continue;
|
|
125
|
+
const label = `${file}:${source.slice(0, offset).split("\n").length}`;
|
|
126
|
+
let targetPath;
|
|
127
|
+
let fragment;
|
|
128
|
+
try {
|
|
129
|
+
const hash = destination.indexOf("#");
|
|
130
|
+
const pathname = (hash < 0 ? destination : destination.slice(0, hash)).split("?")[0];
|
|
131
|
+
targetPath = pathname ? decodeURIComponent(pathname.replace(/\\([\\()[\] ])/g, "$1")) : "";
|
|
132
|
+
fragment = hash < 0 ? "" : decodeURIComponent(destination.slice(hash + 1));
|
|
133
|
+
} catch {
|
|
134
|
+
issues.push(`${label}: invalid URL encoding in ${destination}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const target = targetPath ? path.resolve(root, path.dirname(file), targetPath) : path.join(root, file);
|
|
138
|
+
const relative = path.relative(root, target).split(path.sep).join("/");
|
|
139
|
+
if (relative === ".." || relative.startsWith("../") || path.isAbsolute(relative) || targetPath.startsWith("/")) {
|
|
140
|
+
issues.push(`${label}: local link escapes package: ${destination}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
|
|
144
|
+
issues.push(`${label}: missing local link target: ${destination}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!packagedFiles.has(relative)) issues.push(`${label}: local link target is not packaged: ${destination}`);
|
|
148
|
+
if (fragment && /\.md$/i.test(target)) {
|
|
149
|
+
if (!anchors.has(target)) anchors.set(target, headingAnchors(fs.readFileSync(target, "utf8")));
|
|
150
|
+
if (!anchors.get(target).has(fragment)) issues.push(`${label}: missing heading anchor: ${destination}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return issues;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { documentationFiles, stripCode, markdownLinks, headingAnchors, validateDocs };
|
|
158
|
+
|
|
159
|
+
if (require.main === module) {
|
|
160
|
+
// The package validator supplies its independent exact publication contract
|
|
161
|
+
// and actual packed paths, so a source-only link cannot pass this command.
|
|
162
|
+
const { spawnSync } = require("node:child_process");
|
|
163
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, "validate-package.js")], { stdio: "inherit" });
|
|
164
|
+
if (result.error) throw result.error;
|
|
165
|
+
process.exit(result.status ?? 1);
|
|
166
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { execFileSync } = require("node:child_process");
|
|
8
|
+
const { gunzipSync } = require("node:zlib");
|
|
9
|
+
const { TextDecoder } = require("node:util");
|
|
10
|
+
const { resolveNpmCommand } = require("./lib/npm-command.js");
|
|
11
|
+
const { validateDocs } = require("./validate-docs.js");
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(__dirname, "..");
|
|
14
|
+
const issues = [];
|
|
15
|
+
|
|
16
|
+
// Keep the publication contract independent of directory contents and user state.
|
|
17
|
+
const skillAssets = {
|
|
18
|
+
"add-feature": [],
|
|
19
|
+
"brainstorm-api": ["assets/api.template.md"],
|
|
20
|
+
"brainstorm-architecture": ["assets/architecture.template.md"],
|
|
21
|
+
"brainstorm-prd": ["assets/PRD.template.md"],
|
|
22
|
+
"brainstorm-rules": ["assets/rules.template.md"],
|
|
23
|
+
"brainstorm-schema": ["assets/schema.template.md"],
|
|
24
|
+
"brainstorm-styleguide": ["assets/StyleGuide.template.md"],
|
|
25
|
+
"brainstorm-task": ["assets/Task.template.md"],
|
|
26
|
+
"bug-fix": ["assets/bug-log.template.md"],
|
|
27
|
+
"code-review": ["references/review-checklist.md"],
|
|
28
|
+
developer: [
|
|
29
|
+
"references/close-phase.md",
|
|
30
|
+
"references/execute-task.md",
|
|
31
|
+
"references/onboarding.md",
|
|
32
|
+
],
|
|
33
|
+
help: [],
|
|
34
|
+
meet: [],
|
|
35
|
+
"quick-dev": [],
|
|
36
|
+
"release-readiness": [],
|
|
37
|
+
"setup-macca-method": [],
|
|
38
|
+
"spec-audit": [],
|
|
39
|
+
"spec-compliance": [],
|
|
40
|
+
"spec-init": [],
|
|
41
|
+
};
|
|
42
|
+
const sharedReferences = [
|
|
43
|
+
"additional-skills", "brainstorm-session", "config-mutation", "finding-format",
|
|
44
|
+
"fix-mode", "human-loop", "implementation-principles", "invocation-policy",
|
|
45
|
+
"language-config", "interaction-contract", "output-ownership", "personas", "planning-principles", "runtime-config",
|
|
46
|
+
"scope-delta", "scope-rules", "skill-catalog", "workspace-safety",
|
|
47
|
+
];
|
|
48
|
+
const requiredFiles = new Set([
|
|
49
|
+
"package.json",
|
|
50
|
+
"README.md",
|
|
51
|
+
"CHANGELOG.md",
|
|
52
|
+
"docs/workflows.md",
|
|
53
|
+
"docs/configuration.md",
|
|
54
|
+
"docs/troubleshooting.md",
|
|
55
|
+
"LICENSE",
|
|
56
|
+
"flow.webp",
|
|
57
|
+
"image-macca-method.webp",
|
|
58
|
+
"bin/macca-method.js",
|
|
59
|
+
".agents/legacy-payloads.json",
|
|
60
|
+
".agents/macca-lock.json",
|
|
61
|
+
".agents/macca-managed-skills.txt",
|
|
62
|
+
...Object.entries(skillAssets).flatMap(([name, assets]) =>
|
|
63
|
+
["SKILL.md", ...assets].map((file) => `.agents/skills/${name}/${file}`),
|
|
64
|
+
),
|
|
65
|
+
...sharedReferences.map((name) => `.agents/skills/_shared/references/${name}.md`),
|
|
66
|
+
".agents/skills/_shared/scripts/validate-skills.py",
|
|
67
|
+
".agents/skills/_shared/scripts/config-validator.js",
|
|
68
|
+
".agents/skills/_shared/scripts/config-file.js",
|
|
69
|
+
".agents/skills/_shared/scripts/read-preferences.js",
|
|
70
|
+
"scripts/lib/npm-command.js",
|
|
71
|
+
...[
|
|
72
|
+
"run-skill-validator", "test-install", "test-installer-safety",
|
|
73
|
+
"test-package-safety", "test-upgrade-legacy", "validate-package",
|
|
74
|
+
"validate-skill-behavior", "validate-docs", "test-config", "test-preferences", "test-cli-setup", "test-npm-command",
|
|
75
|
+
].map((name) => `scripts/${name}.js`),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function sorted(values) {
|
|
79
|
+
return [...values].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readArchive(tarballPath) {
|
|
83
|
+
// Deliberately bounded to npm's regular-file ustar/local-PAX payloads. No
|
|
84
|
+
// extraction, links, global overrides, GNU extensions, or candidate execution.
|
|
85
|
+
const fail = (message) => { throw new Error(`Invalid package archive: ${message}`); };
|
|
86
|
+
const decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
87
|
+
const text = (bytes) => {
|
|
88
|
+
const end = bytes.indexOf(0);
|
|
89
|
+
if (end !== -1 && bytes.subarray(end).some((byte) => byte !== 0)) {
|
|
90
|
+
fail("ambiguous tar string field");
|
|
91
|
+
}
|
|
92
|
+
return decode(end === -1 ? bytes : bytes.subarray(0, end));
|
|
93
|
+
};
|
|
94
|
+
const octal = (bytes) => {
|
|
95
|
+
const value = decode(bytes).replace(/\0+$/, "").trim();
|
|
96
|
+
if (!/^[0-7]+$/.test(value)) fail("unsupported tar numeric field");
|
|
97
|
+
const result = Number.parseInt(value, 8);
|
|
98
|
+
if (!Number.isSafeInteger(result)) fail("tar numeric field overflow");
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
const metadataPaths = new Set([
|
|
102
|
+
"package.json", ".agents/macca-lock.json", ".agents/macca-managed-skills.txt",
|
|
103
|
+
]);
|
|
104
|
+
if (fs.statSync(tarballPath).size > 16 * 1024 * 1024) fail("compressed size exceeds 16 MiB");
|
|
105
|
+
const tar = gunzipSync(fs.readFileSync(tarballPath), { maxOutputLength: 32 * 1024 * 1024 });
|
|
106
|
+
if (tar.length % 512 !== 0) fail("truncated tar block");
|
|
107
|
+
const files = [];
|
|
108
|
+
const metadata = new Map();
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
let pax = null;
|
|
111
|
+
let offset = 0;
|
|
112
|
+
let entries = 0;
|
|
113
|
+
while (offset + 512 <= tar.length) {
|
|
114
|
+
const header = tar.subarray(offset, offset + 512);
|
|
115
|
+
if (header.every((byte) => byte === 0)) {
|
|
116
|
+
if (pax || offset + 1024 > tar.length || tar.subarray(offset).some((byte) => byte !== 0)) {
|
|
117
|
+
fail("ambiguous or incomplete tar terminator");
|
|
118
|
+
}
|
|
119
|
+
return { files, metadata };
|
|
120
|
+
}
|
|
121
|
+
if (++entries > 4096) fail("too many tar entries");
|
|
122
|
+
const checksum = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
|
|
123
|
+
// Checksum fields conventionally end in NUL followed by a space.
|
|
124
|
+
const storedChecksum = decode(header.subarray(148, 156)).replace(/[\0 ]+$/, "");
|
|
125
|
+
if (!/^[0-7]+$/.test(storedChecksum) || Number.parseInt(storedChecksum, 8) !== checksum) {
|
|
126
|
+
fail("tar checksum mismatch");
|
|
127
|
+
}
|
|
128
|
+
if (text(header.subarray(257, 263)) !== "ustar" || decode(header.subarray(263, 265)) !== "00") {
|
|
129
|
+
fail("unsupported tar format");
|
|
130
|
+
}
|
|
131
|
+
const size = octal(header.subarray(124, 136));
|
|
132
|
+
const bodyStart = offset + 512;
|
|
133
|
+
offset = bodyStart + Math.ceil(size / 512) * 512;
|
|
134
|
+
if (offset > tar.length) fail("truncated tar entry");
|
|
135
|
+
const body = tar.subarray(bodyStart, bodyStart + size);
|
|
136
|
+
const type = header[156];
|
|
137
|
+
if (type === 120) { // Local PAX extended header.
|
|
138
|
+
if (pax || size > 64 * 1024) fail("ambiguous or oversized PAX header");
|
|
139
|
+
pax = new Map();
|
|
140
|
+
let cursor = 0;
|
|
141
|
+
while (cursor < body.length) {
|
|
142
|
+
const space = body.indexOf(32, cursor);
|
|
143
|
+
if (space === -1) fail("malformed PAX length");
|
|
144
|
+
const lengthText = decode(body.subarray(cursor, space));
|
|
145
|
+
const length = Number(lengthText);
|
|
146
|
+
if (!/^[1-9][0-9]*$/.test(lengthText) || !Number.isSafeInteger(length) ||
|
|
147
|
+
length <= space - cursor + 1 || cursor + length > body.length || body[cursor + length - 1] !== 10) {
|
|
148
|
+
fail("malformed PAX record");
|
|
149
|
+
}
|
|
150
|
+
const record = decode(body.subarray(space + 1, cursor + length - 1));
|
|
151
|
+
const equal = record.indexOf("=");
|
|
152
|
+
const key = record.slice(0, equal);
|
|
153
|
+
if (equal < 1 || pax.has(key) || ![
|
|
154
|
+
"path", "size", "mtime", "atime", "ctime", "uid", "gid", "uname", "gname",
|
|
155
|
+
"SCHILY.dev", "SCHILY.ino", "SCHILY.nlink",
|
|
156
|
+
].includes(key) || /[\0\r\n]/.test(record)) fail("unsupported or ambiguous PAX attribute");
|
|
157
|
+
pax.set(key, record.slice(equal + 1));
|
|
158
|
+
cursor += length;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (type !== 0 && type !== 48) fail("unsupported tar entry type");
|
|
163
|
+
if (text(header.subarray(157, 257))) fail("regular file has a link target");
|
|
164
|
+
if (pax?.has("size") && pax.get("size") !== String(size)) fail("ambiguous PAX size");
|
|
165
|
+
const prefix = text(header.subarray(345, 500));
|
|
166
|
+
const name = text(header.subarray(0, 100));
|
|
167
|
+
const fullPath = pax?.get("path") ?? (prefix ? `${prefix}/${name}` : name);
|
|
168
|
+
pax = null;
|
|
169
|
+
if (!fullPath.startsWith("package/") || fullPath.includes("\\") ||
|
|
170
|
+
fullPath.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
171
|
+
fail("non-canonical package path");
|
|
172
|
+
}
|
|
173
|
+
const relative = fullPath.slice("package/".length);
|
|
174
|
+
if (seen.has(relative)) fail("duplicate package path");
|
|
175
|
+
seen.add(relative);
|
|
176
|
+
files.push({ path: relative });
|
|
177
|
+
if (metadataPaths.has(relative)) {
|
|
178
|
+
if (size > 256 * 1024) fail("metadata exceeds 256 KiB");
|
|
179
|
+
metadata.set(relative, decode(body));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
fail("missing tar terminator");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const args = process.argv.slice(2);
|
|
186
|
+
if (args.length > 1 || (args.length === 1 && !fs.statSync(path.resolve(args[0])).isFile())) {
|
|
187
|
+
throw new Error("Usage: node scripts/validate-package.js [local-tarball.tgz]");
|
|
188
|
+
}
|
|
189
|
+
const expectedPackage = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
190
|
+
const archive = args.length ? readArchive(path.resolve(args[0])) : null;
|
|
191
|
+
function readMetadata(relative) {
|
|
192
|
+
if (!archive) return fs.readFileSync(path.join(root, relative), "utf8");
|
|
193
|
+
if (!archive.metadata.has(relative)) throw new Error(`published package is missing ${relative}`);
|
|
194
|
+
return archive.metadata.get(relative);
|
|
195
|
+
}
|
|
196
|
+
const packageJson = archive ? JSON.parse(readMetadata("package.json")) : expectedPackage;
|
|
197
|
+
const lock = JSON.parse(readMetadata(".agents/macca-lock.json"));
|
|
198
|
+
const manifestSkills = readMetadata(".agents/macca-managed-skills.txt")
|
|
199
|
+
.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
200
|
+
const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
|
|
201
|
+
|
|
202
|
+
if (packageFiles.includes(".agents/")) {
|
|
203
|
+
issues.push("package files must not include the broad .agents/ directory");
|
|
204
|
+
}
|
|
205
|
+
for (const file of [
|
|
206
|
+
"docs/workflows.md", "docs/configuration.md", "docs/troubleshooting.md", "scripts/validate-docs.js",
|
|
207
|
+
"CHANGELOG.md", ".agents/skills/_shared/scripts/config-validator.js",
|
|
208
|
+
".agents/skills/_shared/scripts/read-preferences.js", "scripts/test-preferences.js",
|
|
209
|
+
]) {
|
|
210
|
+
if (!packageFiles.includes(file)) issues.push(`package files must explicitly include ${file}`);
|
|
211
|
+
}
|
|
212
|
+
if (packageFiles.some((file) => /^\/?docs\/?$/.test(file) || /^\/?docs\/.*[*?\[\]{}]/.test(file))) {
|
|
213
|
+
issues.push("package files must not include a broad docs directory or glob");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (lock.version !== expectedPackage.version) {
|
|
217
|
+
issues.push(
|
|
218
|
+
`package/lock version mismatch: package=${expectedPackage.version} lock=${lock.version}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!Array.isArray(lock.skills) || JSON.stringify(sorted(lock.skills)) !== JSON.stringify(sorted(manifestSkills))) {
|
|
223
|
+
issues.push("macca-lock.json skills do not match macca-managed-skills.txt");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
JSON.stringify(sorted(manifestSkills)) !==
|
|
228
|
+
JSON.stringify(sorted(["_shared", ...Object.keys(skillAssets)]))
|
|
229
|
+
) {
|
|
230
|
+
issues.push("managed skills do not match the official package contract");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function listPackageFiles() {
|
|
234
|
+
// Disable lifecycle hooks so validation cannot recursively invoke itself.
|
|
235
|
+
const npm = resolveNpmCommand(["pack", root, "--dry-run", "--json", "--ignore-scripts"]);
|
|
236
|
+
return JSON.parse(execFileSync(npm.command, npm.args, {
|
|
237
|
+
cwd: root,
|
|
238
|
+
encoding: "utf8",
|
|
239
|
+
shell: false,
|
|
240
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const packageList = archive ? [{ ...packageJson, files: archive.files }] : listPackageFiles();
|
|
245
|
+
if (!Array.isArray(packageList) || packageList.length !== 1 || !Array.isArray(packageList[0].files)) {
|
|
246
|
+
throw new Error("npm pack did not return exactly one package file listing");
|
|
247
|
+
}
|
|
248
|
+
const packed = packageList[0];
|
|
249
|
+
const files = packed.files.map((entry) => entry.path);
|
|
250
|
+
const fileSet = new Set(files);
|
|
251
|
+
|
|
252
|
+
if (packed.name !== expectedPackage.name || packed.version !== expectedPackage.version) {
|
|
253
|
+
issues.push("tarball name/version do not match package.json");
|
|
254
|
+
}
|
|
255
|
+
if (fileSet.size !== files.length) {
|
|
256
|
+
issues.push("published package contains duplicate paths");
|
|
257
|
+
}
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
if (!requiredFiles.has(file)) {
|
|
260
|
+
issues.push(`published package includes unauthorized file ${file}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const requiredFile of requiredFiles) {
|
|
265
|
+
if (!fileSet.has(requiredFile)) {
|
|
266
|
+
issues.push(`published package is missing ${requiredFile}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Source links must target both a required publication file and the actual
|
|
271
|
+
// pack listing. Candidate archives remain passive: never execute their code.
|
|
272
|
+
if (!archive) {
|
|
273
|
+
issues.push(...validateDocs(root, new Set([...requiredFiles].filter((file) => fileSet.has(file)))));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (issues.length > 0) {
|
|
277
|
+
process.stderr.write(
|
|
278
|
+
`Package contract findings:\n${issues.map((issue) => `- ${issue}`).join("\n")}\n`,
|
|
279
|
+
);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
process.stdout.write(
|
|
284
|
+
`OK: package metadata and ${files.length} tarball files validated${archive ? "" : "; README and 3 guides have valid packaged links/anchors"}\n`,
|
|
285
|
+
);
|