verity-framework 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/commands/verity/architect.md +58 -0
- package/commands/verity/build.md +61 -0
- package/commands/verity/docs.md +37 -0
- package/commands/verity/golive.md +32 -0
- package/commands/verity/map.md +22 -0
- package/commands/verity/plan.md +67 -0
- package/commands/verity/review.md +50 -0
- package/commands/verity/security.md +36 -0
- package/commands/verity/ship.md +67 -0
- package/commands/verity/sre.md +31 -0
- package/commands/verity/test.md +29 -0
- package/commands/verity/verify.md +31 -0
- package/commands/verity/vision.md +55 -0
- package/package.json +31 -0
- package/verity/bin/lib/adr.cjs +67 -0
- package/verity/bin/lib/catalog.cjs +81 -0
- package/verity/bin/lib/config.cjs +119 -0
- package/verity/bin/lib/contract.cjs +57 -0
- package/verity/bin/lib/core.cjs +63 -0
- package/verity/bin/lib/golive.cjs +49 -0
- package/verity/bin/lib/handoff.cjs +72 -0
- package/verity/bin/lib/identity.cjs +112 -0
- package/verity/bin/lib/install.cjs +109 -0
- package/verity/bin/lib/ledger.cjs +244 -0
- package/verity/bin/lib/map.cjs +77 -0
- package/verity/bin/lib/recovery.cjs +37 -0
- package/verity/bin/lib/release.cjs +131 -0
- package/verity/bin/lib/review.cjs +74 -0
- package/verity/bin/lib/scaffold.cjs +66 -0
- package/verity/bin/lib/security.cjs +44 -0
- package/verity/bin/lib/smoke.cjs +170 -0
- package/verity/bin/lib/stage.cjs +180 -0
- package/verity/bin/lib/status.cjs +117 -0
- package/verity/bin/verity.cjs +190 -0
- package/verity/design-guides/contracts-first.md +32 -0
- package/verity/design-guides/features/helper-bot.md +61 -0
- package/verity/design-guides/stack-and-topology.md +38 -0
- package/verity/templates/LICENSE.tmpl +21 -0
- package/verity/templates/README.md.tmpl +14 -0
- package/verity/templates/STATUS.md.tmpl +27 -0
- package/verity/templates/adr.md.tmpl +21 -0
- package/verity/templates/bug_report.yml.tmpl +44 -0
- package/verity/templates/ci.yml.tmpl +36 -0
- package/verity/templates/contract.md.tmpl +21 -0
- package/verity/templates/gitignore.tmpl +9 -0
- package/verity/templates/handoff-brief.md.tmpl +32 -0
- package/verity/templates/handoff-readme.md.tmpl +21 -0
- package/verity/templates/recovery-plan.md.tmpl +29 -0
- package/verity/templates/security-invariants.md.tmpl +14 -0
- package/verity/templates/smoke.json.tmpl +21 -0
- package/verity/templates/stage.md.tmpl +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "verity-framework",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Verity — a CI/CD-native, GitHub-native, production-lifecycle AI software delivery framework.",
|
|
5
|
+
"keywords": ["verity", "ai", "ci-cd", "github", "devops", "agent", "framework"],
|
|
6
|
+
"author": "Sean Mahoney",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/seanerama/verity-framework#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/seanerama/verity-framework.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/seanerama/verity-framework/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"verity": "verity/bin/verity.cjs"
|
|
18
|
+
},
|
|
19
|
+
"files": ["verity", "commands", "README.md", "LICENSE"],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=16.7.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node scripts/run-tests.cjs",
|
|
25
|
+
"lint": "biome ci .",
|
|
26
|
+
"prepublishOnly": "npm run lint && npm test"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@biomejs/biome": "1.9.4"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Architecture Decision Records — docs/adr/NNNN-slug.md (framework-spec.md §4.3).
|
|
2
|
+
// Every architectural decision/deviation is an append-only, numbered ADR.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { generateSlug, render } = require('./core.cjs');
|
|
7
|
+
|
|
8
|
+
const TEMPLATE = path.join(__dirname, '..', '..', 'templates', 'adr.md.tmpl');
|
|
9
|
+
|
|
10
|
+
function adrDir(cwd) {
|
|
11
|
+
return path.join(cwd, 'docs', 'adr');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function nextNumber(cwd) {
|
|
15
|
+
const dir = adrDir(cwd);
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const nums = fs
|
|
20
|
+
.readdirSync(dir)
|
|
21
|
+
.map((name) => Number.parseInt(name.slice(0, 4), 10))
|
|
22
|
+
.filter((n) => Number.isFinite(n));
|
|
23
|
+
return nums.length > 0 ? Math.max(...nums) + 1 : 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function create(cwd, title, opts = {}) {
|
|
27
|
+
if (!title) {
|
|
28
|
+
throw new Error('adr new requires a title');
|
|
29
|
+
}
|
|
30
|
+
const padded = String(nextNumber(cwd)).padStart(4, '0');
|
|
31
|
+
const slug = generateSlug(title) || 'decision';
|
|
32
|
+
const file = path.join(adrDir(cwd), `${padded}-${slug}.md`);
|
|
33
|
+
fs.mkdirSync(adrDir(cwd), { recursive: true });
|
|
34
|
+
const content = render(fs.readFileSync(TEMPLATE, 'utf8'), {
|
|
35
|
+
number: padded,
|
|
36
|
+
title,
|
|
37
|
+
status: opts.status || 'Proposed',
|
|
38
|
+
date: new Date().toISOString().slice(0, 10),
|
|
39
|
+
});
|
|
40
|
+
fs.writeFileSync(file, content);
|
|
41
|
+
return { number: padded, title, path: file };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function list(cwd) {
|
|
45
|
+
const dir = adrDir(cwd);
|
|
46
|
+
const adrs = fs.existsSync(dir)
|
|
47
|
+
? fs
|
|
48
|
+
.readdirSync(dir)
|
|
49
|
+
.filter((n) => n.endsWith('.md'))
|
|
50
|
+
.sort()
|
|
51
|
+
: [];
|
|
52
|
+
return { adrs };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dispatch(args, flags) {
|
|
56
|
+
const cwd = flags.cwd || process.cwd();
|
|
57
|
+
const verb = args[0];
|
|
58
|
+
if (verb === 'new') {
|
|
59
|
+
return create(cwd, args[1], { status: flags.status });
|
|
60
|
+
}
|
|
61
|
+
if (verb === 'list') {
|
|
62
|
+
return list(cwd);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`unknown adr verb: ${verb || '(none)'} — use new|list`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { adrDir, nextNumber, create, list, dispatch };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Design guides + drop-in feature catalog (framework-spec.md §4.5).
|
|
2
|
+
// Guides are RECOMMENDATIONS the Architect reviews; features are pre-packaged
|
|
3
|
+
// stage-sets the Architect can offer. Both ship as sample content under
|
|
4
|
+
// design-guides/ and are read-only discovery here (org override is future config).
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
// Ships inside the package internals (verity/design-guides) so it travels with
|
|
9
|
+
// `verity install` and npm publish.
|
|
10
|
+
const GUIDES_DIR = path.join(__dirname, '..', '..', 'design-guides');
|
|
11
|
+
const FEATURES_DIR = path.join(GUIDES_DIR, 'features');
|
|
12
|
+
|
|
13
|
+
function parseFrontmatter(text) {
|
|
14
|
+
const meta = {};
|
|
15
|
+
const match = text.match(/^---\n([\s\S]*?)\n---/);
|
|
16
|
+
if (match) {
|
|
17
|
+
for (const line of match[1].split('\n')) {
|
|
18
|
+
const idx = line.indexOf(':');
|
|
19
|
+
if (idx > 0) {
|
|
20
|
+
meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return meta;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function listDir(dir) {
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return fs
|
|
32
|
+
.readdirSync(dir)
|
|
33
|
+
.filter((name) => name.endsWith('.md'))
|
|
34
|
+
.sort()
|
|
35
|
+
.map((name) => {
|
|
36
|
+
const meta = parseFrontmatter(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
37
|
+
return { id: name.replace(/\.md$/, ''), title: meta.title || name, ...meta };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function showFile(dir, id) {
|
|
42
|
+
if (!id) {
|
|
43
|
+
throw new Error('show requires an id');
|
|
44
|
+
}
|
|
45
|
+
const file = path.join(dir, `${id}.md`);
|
|
46
|
+
if (!fs.existsSync(file)) {
|
|
47
|
+
throw new Error(`not found: ${id}`);
|
|
48
|
+
}
|
|
49
|
+
return { id, content: fs.readFileSync(file, 'utf8') };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function guidesDispatch(args) {
|
|
53
|
+
const verb = args[0];
|
|
54
|
+
if (verb === 'list') {
|
|
55
|
+
return { guides: listDir(GUIDES_DIR) };
|
|
56
|
+
}
|
|
57
|
+
if (verb === 'show') {
|
|
58
|
+
return showFile(GUIDES_DIR, args[1]);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`unknown guides verb: ${verb || '(none)'} — use list|show`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function featureDispatch(args) {
|
|
64
|
+
const verb = args[0];
|
|
65
|
+
if (verb === 'list') {
|
|
66
|
+
return { features: listDir(FEATURES_DIR) };
|
|
67
|
+
}
|
|
68
|
+
if (verb === 'show') {
|
|
69
|
+
return showFile(FEATURES_DIR, args[1]);
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`unknown feature verb: ${verb || '(none)'} — use list|show`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
GUIDES_DIR,
|
|
76
|
+
FEATURES_DIR,
|
|
77
|
+
parseFrontmatter,
|
|
78
|
+
listDir,
|
|
79
|
+
guidesDispatch,
|
|
80
|
+
featureDispatch,
|
|
81
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Verity project config — `.verity/config.json`. Carried pattern from 1.4.
|
|
2
|
+
// AUTHOR/DERIVE: writes/reads the project's config knobs (NOT integration state).
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
version: '1.0',
|
|
8
|
+
model_profile: 'balanced',
|
|
9
|
+
// Release/Deploy Operator gate: confirm (human) by default, or auto. (spec §6)
|
|
10
|
+
prod_promote: 'confirm',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function configPath(cwd) {
|
|
14
|
+
return path.join(cwd, '.verity', 'config.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readConfig(cwd) {
|
|
18
|
+
const p = configPath(cwd);
|
|
19
|
+
if (!fs.existsSync(p)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensure(cwd) {
|
|
26
|
+
const p = configPath(cwd);
|
|
27
|
+
if (fs.existsSync(p)) {
|
|
28
|
+
return { created: false, path: p, config: readConfig(cwd) };
|
|
29
|
+
}
|
|
30
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
31
|
+
const cfg = { ...DEFAULTS };
|
|
32
|
+
fs.writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
33
|
+
return { created: true, path: p, config: cfg };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getAt(obj, dotted) {
|
|
37
|
+
let node = obj;
|
|
38
|
+
for (const key of dotted.split('.')) {
|
|
39
|
+
if (node === null || node === undefined) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
node = node[key];
|
|
43
|
+
}
|
|
44
|
+
return node;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setAt(obj, dotted, value) {
|
|
48
|
+
const keys = dotted.split('.');
|
|
49
|
+
let node = obj;
|
|
50
|
+
for (let i = 0; i < keys.length - 1; i += 1) {
|
|
51
|
+
if (node[keys[i]] === null || typeof node[keys[i]] !== 'object') {
|
|
52
|
+
node[keys[i]] = {};
|
|
53
|
+
}
|
|
54
|
+
node = node[keys[i]];
|
|
55
|
+
}
|
|
56
|
+
node[keys[keys.length - 1]] = value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function coerce(value) {
|
|
60
|
+
if (value === 'true') {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (value === 'false') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (/^-?\d+$/.test(value)) {
|
|
67
|
+
return Number(value);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function get(cwd, key) {
|
|
73
|
+
const cfg = readConfig(cwd) || { ...DEFAULTS };
|
|
74
|
+
if (!key) {
|
|
75
|
+
return { config: cfg };
|
|
76
|
+
}
|
|
77
|
+
const value = getAt(cfg, key);
|
|
78
|
+
return {
|
|
79
|
+
key,
|
|
80
|
+
value,
|
|
81
|
+
raw: value === null || value === undefined ? '' : String(value),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function set(cwd, key, rawValue) {
|
|
86
|
+
const { config: cfg } = ensure(cwd);
|
|
87
|
+
const value = coerce(rawValue);
|
|
88
|
+
setAt(cfg, key, value);
|
|
89
|
+
fs.writeFileSync(configPath(cwd), `${JSON.stringify(cfg, null, 2)}\n`);
|
|
90
|
+
return { key, value, path: configPath(cwd) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function dispatch(args, flags) {
|
|
94
|
+
const verb = args[0];
|
|
95
|
+
const cwd = flags.cwd || process.cwd();
|
|
96
|
+
if (verb === 'ensure') {
|
|
97
|
+
return ensure(cwd);
|
|
98
|
+
}
|
|
99
|
+
if (verb === 'get') {
|
|
100
|
+
return get(cwd, args[1]);
|
|
101
|
+
}
|
|
102
|
+
if (verb === 'set') {
|
|
103
|
+
return set(cwd, args[1], args[2]);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`unknown config verb: ${verb || '(none)'} — use ensure|get|set`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
DEFAULTS,
|
|
110
|
+
configPath,
|
|
111
|
+
readConfig,
|
|
112
|
+
ensure,
|
|
113
|
+
get,
|
|
114
|
+
set,
|
|
115
|
+
dispatch,
|
|
116
|
+
getAt,
|
|
117
|
+
setAt,
|
|
118
|
+
coerce,
|
|
119
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Interface contracts — contracts/<name>.md (framework-spec.md §4.3).
|
|
2
|
+
// Contracts are MANDATES, frozen early. A change is ADDITIVE only; a breaking
|
|
3
|
+
// change is a NEW contract, not an edit — so `new` refuses to overwrite.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const { validateSlug, render } = require('./core.cjs');
|
|
8
|
+
|
|
9
|
+
const TEMPLATE = path.join(__dirname, '..', '..', 'templates', 'contract.md.tmpl');
|
|
10
|
+
|
|
11
|
+
function contractsDir(cwd) {
|
|
12
|
+
return path.join(cwd, 'contracts');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function create(cwd, name) {
|
|
16
|
+
if (!name) {
|
|
17
|
+
throw new Error('contract new requires a name');
|
|
18
|
+
}
|
|
19
|
+
const v = validateSlug(name);
|
|
20
|
+
if (!v.valid) {
|
|
21
|
+
throw new Error(`invalid contract name "${name}": ${v.issues.join('; ')}`);
|
|
22
|
+
}
|
|
23
|
+
const file = path.join(contractsDir(cwd), `${name}.md`);
|
|
24
|
+
if (fs.existsSync(file)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`contract "${name}" already exists — contracts are frozen; a change is a NEW contract, not an edit`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
fs.mkdirSync(contractsDir(cwd), { recursive: true });
|
|
30
|
+
fs.writeFileSync(file, render(fs.readFileSync(TEMPLATE, 'utf8'), { name }));
|
|
31
|
+
return { name, path: file };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function list(cwd) {
|
|
35
|
+
const dir = contractsDir(cwd);
|
|
36
|
+
const contracts = fs.existsSync(dir)
|
|
37
|
+
? fs
|
|
38
|
+
.readdirSync(dir)
|
|
39
|
+
.filter((n) => n.endsWith('.md'))
|
|
40
|
+
.sort()
|
|
41
|
+
: [];
|
|
42
|
+
return { contracts };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dispatch(args, flags) {
|
|
46
|
+
const cwd = flags.cwd || process.cwd();
|
|
47
|
+
const verb = args[0];
|
|
48
|
+
if (verb === 'new') {
|
|
49
|
+
return create(cwd, args[1]);
|
|
50
|
+
}
|
|
51
|
+
if (verb === 'list') {
|
|
52
|
+
return list(cwd);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`unknown contract verb: ${verb || '(none)'} — use new|list`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { contractsDir, create, list, dispatch };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Verity core utilities (the seed of the `identity` command, Role 1 / Vision).
|
|
2
|
+
//
|
|
3
|
+
// validateSlug enforces the UNION of downstream constraints the slug must satisfy,
|
|
4
|
+
// because it becomes the identity key threaded through repo name, package name,
|
|
5
|
+
// container-registry image path, DNS label, branch names, env names, and secret
|
|
6
|
+
// names (see framework-spec.md §4.1). The strictest common denominator is:
|
|
7
|
+
// lowercase, hyphen-separated, starts with a letter, no underscores, <= 63 chars.
|
|
8
|
+
|
|
9
|
+
function validateSlug(slug) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const s = String(slug ?? '');
|
|
12
|
+
|
|
13
|
+
if (s.length === 0) {
|
|
14
|
+
issues.push('empty');
|
|
15
|
+
}
|
|
16
|
+
if (s.length > 63) {
|
|
17
|
+
issues.push('too long (max 63 chars — DNS label limit)');
|
|
18
|
+
}
|
|
19
|
+
if (!/^[a-z]/.test(s)) {
|
|
20
|
+
issues.push('must start with a lowercase letter');
|
|
21
|
+
}
|
|
22
|
+
if (/[A-Z]/.test(s)) {
|
|
23
|
+
issues.push('no uppercase (container/package registries require lowercase)');
|
|
24
|
+
}
|
|
25
|
+
if (s.includes('_')) {
|
|
26
|
+
issues.push('no underscores (DNS/registry-unsafe)');
|
|
27
|
+
}
|
|
28
|
+
if (!/^[a-z0-9-]*$/.test(s)) {
|
|
29
|
+
issues.push('only lowercase letters, digits, and hyphens allowed');
|
|
30
|
+
}
|
|
31
|
+
if (/--/.test(s)) {
|
|
32
|
+
issues.push('no consecutive hyphens');
|
|
33
|
+
}
|
|
34
|
+
if (/-$/.test(s)) {
|
|
35
|
+
issues.push('must not end with a hyphen');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { valid: issues.length === 0, issues };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Turn arbitrary text into a slug candidate (lowercase, hyphen-joined, trimmed,
|
|
42
|
+
// capped at 63). The result may still fail validateSlug (e.g. a leading digit) —
|
|
43
|
+
// the caller validates and surfaces issues; this only proposes.
|
|
44
|
+
function generateSlug(text) {
|
|
45
|
+
return String(text ?? '')
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
49
|
+
.replace(/^-+/, '')
|
|
50
|
+
.replace(/-+$/, '')
|
|
51
|
+
.slice(0, 63)
|
|
52
|
+
.replace(/-+$/, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Render a template: replace {{key}} (word chars only, no spaces) so GitHub Actions
|
|
56
|
+
// ${{ ... }} expressions — which always contain spaces/dots — pass through untouched.
|
|
57
|
+
function render(tmpl, vars) {
|
|
58
|
+
return String(tmpl).replace(/\{\{(\w+)\}\}/g, (match, key) =>
|
|
59
|
+
key in vars ? String(vars[key] ?? '') : match,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { validateSlug, generateSlug, render };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Pre-go-live / first-real-data gate (framework-spec.md §6). A BLOCKING checklist
|
|
2
|
+
// before the project accepts real data/users (Security Auditor + SRE jointly). Auto-
|
|
3
|
+
// checks what's derivable; lists the manual gates that need human confirmation.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const security = require('./security.cjs');
|
|
8
|
+
const status = require('./status.cjs');
|
|
9
|
+
|
|
10
|
+
function check(cwd) {
|
|
11
|
+
const runtime = status.read(cwd);
|
|
12
|
+
const items = [
|
|
13
|
+
{
|
|
14
|
+
item: 'Security invariants defined (docs/security-invariants.md)',
|
|
15
|
+
ok: Boolean(security.read(cwd)),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
item: 'Secret locations recorded in STATUS (runtime.json)',
|
|
19
|
+
ok: Array.isArray(runtime.secret_locations) && runtime.secret_locations.length > 0,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
item: 'Recovery plan present (recovery-plan.md)',
|
|
23
|
+
ok: fs.existsSync(path.join(cwd, 'recovery-plan.md')),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const manual = [
|
|
27
|
+
'Secrets rotated (no dev/exposed credentials)',
|
|
28
|
+
'Throwaway accounts removed',
|
|
29
|
+
'Cross-user data isolation verified',
|
|
30
|
+
'Backup coverage for ALL persistent state (no silent gaps)',
|
|
31
|
+
'Security deep-audit sign-off',
|
|
32
|
+
];
|
|
33
|
+
const autoPass = items.every((i) => i.ok);
|
|
34
|
+
return {
|
|
35
|
+
items,
|
|
36
|
+
manual,
|
|
37
|
+
autoPass,
|
|
38
|
+
ready: autoPass,
|
|
39
|
+
raw: autoPass
|
|
40
|
+
? 'auto-checks pass — now confirm the manual gates before go-live'
|
|
41
|
+
: 'BLOCKED: resolve the failing auto-checks',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function dispatch(args, flags) {
|
|
46
|
+
return check(flags.cwd || process.cwd());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { check, dispatch };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Technical Writer (framework-spec.md §6, Role 13) — handoff briefs in docs/handoff/.
|
|
2
|
+
// The brief's highest-leverage element is the "settled decisions — do NOT re-litigate"
|
|
3
|
+
// section (H6); the reading-order README is what makes zero-setup rejoin real (H5).
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const { validateSlug, render } = require('./core.cjs');
|
|
8
|
+
|
|
9
|
+
const README_TEMPLATE = path.join(__dirname, '..', '..', 'templates', 'handoff-readme.md.tmpl');
|
|
10
|
+
const BRIEF_TEMPLATE = path.join(__dirname, '..', '..', 'templates', 'handoff-brief.md.tmpl');
|
|
11
|
+
|
|
12
|
+
function handoffDir(cwd) {
|
|
13
|
+
return path.join(cwd, 'docs', 'handoff');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureReadme(cwd) {
|
|
17
|
+
const p = path.join(handoffDir(cwd), 'README.md');
|
|
18
|
+
if (fs.existsSync(p)) {
|
|
19
|
+
return { created: false, path: p };
|
|
20
|
+
}
|
|
21
|
+
fs.mkdirSync(handoffDir(cwd), { recursive: true });
|
|
22
|
+
fs.writeFileSync(p, fs.readFileSync(README_TEMPLATE, 'utf8'));
|
|
23
|
+
return { created: true, path: p };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function create(cwd, slug, opts = {}) {
|
|
27
|
+
if (!slug) {
|
|
28
|
+
throw new Error('handoff new requires a slug');
|
|
29
|
+
}
|
|
30
|
+
const v = validateSlug(slug);
|
|
31
|
+
if (!v.valid) {
|
|
32
|
+
throw new Error(`invalid handoff slug "${slug}": ${v.issues.join('; ')}`);
|
|
33
|
+
}
|
|
34
|
+
ensureReadme(cwd);
|
|
35
|
+
const p = path.join(handoffDir(cwd), `${slug}.md`);
|
|
36
|
+
if (fs.existsSync(p) && !opts.force) {
|
|
37
|
+
throw new Error(`handoff brief "${slug}" already exists`);
|
|
38
|
+
}
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
p,
|
|
41
|
+
render(fs.readFileSync(BRIEF_TEMPLATE, 'utf8'), { slug, title: opts.title || slug }),
|
|
42
|
+
);
|
|
43
|
+
return { created: true, path: p };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function list(cwd) {
|
|
47
|
+
const dir = handoffDir(cwd);
|
|
48
|
+
const briefs = fs.existsSync(dir)
|
|
49
|
+
? fs
|
|
50
|
+
.readdirSync(dir)
|
|
51
|
+
.filter((n) => n.endsWith('.md') && n !== 'README.md')
|
|
52
|
+
.sort()
|
|
53
|
+
: [];
|
|
54
|
+
return { briefs };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dispatch(args, flags) {
|
|
58
|
+
const cwd = flags.cwd || process.cwd();
|
|
59
|
+
const verb = args[0];
|
|
60
|
+
if (verb === 'new') {
|
|
61
|
+
return create(cwd, args[1], { title: flags.title, force: Boolean(flags.force) });
|
|
62
|
+
}
|
|
63
|
+
if (verb === 'list') {
|
|
64
|
+
return list(cwd);
|
|
65
|
+
}
|
|
66
|
+
if (verb === 'readme') {
|
|
67
|
+
return ensureReadme(cwd);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`unknown handoff verb: ${verb || '(none)'} — use new|list|readme`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { handoffDir, ensureReadme, create, list, dispatch };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Verity identity manifest — `.verity/identity.json` (framework-spec.md §4.1).
|
|
2
|
+
// The slug is the identity key threaded through repo/package/image/dns/env/secret
|
|
3
|
+
// names. It is LOCKED ONCE and immutable; renaming later is a migration, not an edit.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const { validateSlug } = require('./core.cjs');
|
|
9
|
+
|
|
10
|
+
function manifestPath(cwd) {
|
|
11
|
+
return path.join(cwd, '.verity', 'identity.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Default command runner: exit 0 → { ok: true }, anything else → { ok: false }.
|
|
15
|
+
// Injectable so availability checks (which shell out to gh/npm) stay unit-testable.
|
|
16
|
+
function defaultRun(cmd, args) {
|
|
17
|
+
try {
|
|
18
|
+
execFileSync(cmd, args, { stdio: 'pipe' });
|
|
19
|
+
return { ok: true };
|
|
20
|
+
} catch {
|
|
21
|
+
return { ok: false };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Best-effort availability: a successful lookup means the name is TAKEN.
|
|
26
|
+
// Caveat: a network/auth failure also returns non-zero, so "available" can be a
|
|
27
|
+
// false positive — surfaced as best-effort, not a guarantee.
|
|
28
|
+
function checkAvailability(slug, opts) {
|
|
29
|
+
const run = opts.run || defaultRun;
|
|
30
|
+
const checks = {};
|
|
31
|
+
checks.npm = { available: !run('npm', ['view', slug, 'version']).ok };
|
|
32
|
+
if (opts.owner) {
|
|
33
|
+
checks.github = {
|
|
34
|
+
available: !run('gh', ['repo', 'view', `${opts.owner}/${slug}`]).ok,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return checks;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function check(slug, opts = {}) {
|
|
41
|
+
const validation = validateSlug(slug);
|
|
42
|
+
const availability = validation.valid ? checkAvailability(slug, opts) : {};
|
|
43
|
+
const values = Object.values(availability).map((c) => c.available);
|
|
44
|
+
const available = values.length > 0 ? values.every(Boolean) : null;
|
|
45
|
+
return {
|
|
46
|
+
slug,
|
|
47
|
+
valid: validation.valid,
|
|
48
|
+
issues: validation.issues,
|
|
49
|
+
availability,
|
|
50
|
+
available,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function lock(cwd, opts) {
|
|
55
|
+
const { name, slug, owner, force } = opts;
|
|
56
|
+
const p = manifestPath(cwd);
|
|
57
|
+
if (fs.existsSync(p) && !force) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`identity already locked at ${p} — renaming is a migration, not an edit (use --force to override)`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const v = validateSlug(slug);
|
|
63
|
+
if (!v.valid) {
|
|
64
|
+
throw new Error(`invalid slug "${slug}": ${v.issues.join('; ')}`);
|
|
65
|
+
}
|
|
66
|
+
const manifest = {
|
|
67
|
+
version: '1.0',
|
|
68
|
+
name: name || slug,
|
|
69
|
+
slug,
|
|
70
|
+
owner: owner || null,
|
|
71
|
+
image_prefix: owner ? `ghcr.io/${owner}/${slug}` : null,
|
|
72
|
+
locked_at: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
75
|
+
fs.writeFileSync(p, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
76
|
+
return { locked: true, path: p, manifest };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function get(cwd, key) {
|
|
80
|
+
const p = manifestPath(cwd);
|
|
81
|
+
if (!fs.existsSync(p)) {
|
|
82
|
+
throw new Error(`no identity manifest at ${p} — run 'verity identity lock' first`);
|
|
83
|
+
}
|
|
84
|
+
const manifest = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
85
|
+
if (!key) {
|
|
86
|
+
return { manifest };
|
|
87
|
+
}
|
|
88
|
+
const value = manifest[key];
|
|
89
|
+
return { key, value, raw: value === null || value === undefined ? '' : String(value) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function dispatch(args, flags) {
|
|
93
|
+
const verb = args[0];
|
|
94
|
+
const cwd = flags.cwd || process.cwd();
|
|
95
|
+
if (verb === 'check') {
|
|
96
|
+
return check(args[1], { owner: flags.owner });
|
|
97
|
+
}
|
|
98
|
+
if (verb === 'lock') {
|
|
99
|
+
return lock(cwd, {
|
|
100
|
+
name: args[1],
|
|
101
|
+
slug: args[2],
|
|
102
|
+
owner: flags.owner,
|
|
103
|
+
force: Boolean(flags.force),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (verb === 'get') {
|
|
107
|
+
return get(cwd, args[1]);
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`unknown identity verb: ${verb || '(none)'} — use check|lock|get`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { manifestPath, check, checkAvailability, lock, get, dispatch };
|