@starci/hfs 4.8.0 → 4.9.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/CHANGELOG.md +6 -0
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +1 -1
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -10
- package/runtime/knowledge/hfs/rules.yaml +23 -0
- package/runtime/modules/kernel/failure-codes.yaml +10 -0
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +4 -4
- package/src/main.mjs +3 -3
- package/sync/index.mjs +14 -1
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- package/runtime/scripts/work/render-tools.mjs +0 -64
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// draw-rationale.mjs — owner ruling 2026-09-27 (draw-rationale-evidence): the owner critiques a drawing from
|
|
3
|
-
// EVIDENCE, never from taste. Every interface.draw shape ships, next to its render source, a rationale.json - an
|
|
4
|
-
// array of decisions
|
|
5
|
-
//
|
|
6
|
-
// {id, selector, kind: element|layout|spacing|type|radius|colour|art, decision, value, because, rules[],
|
|
7
|
-
// alternativesRejected[{option, why}], source}
|
|
8
|
-
//
|
|
9
|
-
// and every visible element and region of the render source carries data-why="<id>[ <id>...]" naming its
|
|
10
|
-
// decisions. draw-render.mjs measures the render itself (measureRationale: getComputedStyle over every visible
|
|
11
|
-
// element) and captures an annotated redline render beside each part (<base>.redline.png: spacing brackets with the
|
|
12
|
-
// value and the rule id, DNA labels at component roots). DRAW_RATIONALE_MISSING refuses, per render source:
|
|
13
|
-
// - no rationale file (<source stem>.rationale.json, else rationale.json beside it), or an entry that is malformed
|
|
14
|
-
// (a field missing, an unknown kind, a duplicate id);
|
|
15
|
-
// - a visible element or region without data-why, or a data-why naming no entry; a region whose decision is not a
|
|
16
|
-
// layout decision; no layout decision stating the region order (its value names the region count);
|
|
17
|
-
// - a grid whose layout decision does not state its column count at a viewport;
|
|
18
|
-
// - a distinct gap/padding/inset, radius, font-size, font-weight or line-height the RENDER uses (measured at every
|
|
19
|
-
// viewport) that no decision of the matching kind states in its value (spacing|layout, radius, type);
|
|
20
|
-
// - a colour the render paints (background, text, border) that no colour decision names (hex, or a --token that
|
|
21
|
-
// resolves to it in the render);
|
|
22
|
-
// - a DNA component or a closed variant value the render uses that no decision names;
|
|
23
|
-
// - an art slot (img, picture, data-asset-slot, data-artwork-slot, a background image) whose decision is not art;
|
|
24
|
-
// - a rules[] entry that resolves to nothing: its first token must be a knowledge id (knowledge/**: GAP-5,
|
|
25
|
-
// PADDING-9 case-1, ui.presentation.gap), a DNA component or closed value (dna:Button.variant=secondary), a
|
|
26
|
-
// brand.direction principle/rubric id (direction:P2, rubric:H3), or an owner ruling (owner:<id> of
|
|
27
|
-
// modules/kernel/owner-rulings.yaml, a ui.review.feedback note id, an answer receipt);
|
|
28
|
-
// - an empty `because`, or one that cites no FR, content or user job;
|
|
29
|
-
// - a part with no measured render (re-render it with draw-render) or no redline render beside it.
|
|
30
|
-
//
|
|
31
|
-
// starci work draw-rationale <render.html> [--rationale <file>] [--ui <ui-record-dir>]
|
|
32
|
-
// [--records <draw-render.json>[,<...>]] [--json]
|
|
33
|
-
import fs from 'node:fs';
|
|
34
|
-
import path from 'node:path';
|
|
35
|
-
import { fileURLToPath } from 'node:url';
|
|
36
|
-
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
37
|
-
import { COMPONENT_ATTR, loadDna, walkElements } from './draw-dna.mjs';
|
|
38
|
-
import { findRationaleIssues } from './rationale-findings.mjs';
|
|
39
|
-
import { bareRuleToken, parseRuleRef } from './draw-rationale-ref.mjs';
|
|
40
|
-
import { list } from '../../lib/list.mjs';
|
|
41
|
-
import { isFile } from '../../lib/fs-kind.mjs'; import { isMain } from '../../lib/is-main.mjs';
|
|
42
|
-
|
|
43
|
-
export { drawRedlines, measureRationale } from './draw-rationale-page.mjs';
|
|
44
|
-
|
|
45
|
-
export const DRAW_RATIONALE_MISSING = 'DRAW_RATIONALE_MISSING';
|
|
46
|
-
const RATIONALE_KINDS = Object.freeze(['element', 'layout', 'spacing', 'type', 'radius', 'colour', 'art']);
|
|
47
|
-
export const WHY_ATTR = 'data-why';
|
|
48
|
-
export const REDLINE_ATTR = 'data-draw-redline';
|
|
49
|
-
export const MEASURE_SCHEMA = 'starci/draw-rationale-measure@1';
|
|
50
|
-
const REQUIRED_FIELDS = Object.freeze(['id', 'selector', 'kind', 'decision', 'value', 'because', 'rules', 'alternativesRejected', 'source']);
|
|
51
|
-
/** Components too small to label in the redline (they are named by their region's label). */
|
|
52
|
-
export const REDLINE_LEAF_COMPONENTS = Object.freeze(['Text', 'Heading', 'Icon', 'Label', 'Description', 'Kbd', 'Badge', 'Chip', 'IconTile', 'TagGroup', 'Tag', 'StateMark', 'MediaFrame', 'Avatar']);
|
|
53
|
-
|
|
54
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
55
|
-
const KNOWLEDGE = path.join(ROOT, 'knowledge');
|
|
56
|
-
const OWNER_RULINGS = path.join(ROOT, 'modules', 'kernel', 'owner-rulings.yaml');
|
|
57
|
-
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return undefined; } };
|
|
58
|
-
const readYamlOr = (f) => { try { return parseYaml(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
59
|
-
const str = (v) => { if (v == null) { return ''; } if (typeof v === 'string') { return v; } if (typeof v === 'number') { return String(v); } return JSON.stringify(v); };
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
62
|
-
// The rationale file
|
|
63
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/** The rationale file of a render source: <stem>.rationale.json beside it, else rationale.json in its directory. */
|
|
66
|
-
export function rationaleFileOf(htmlFile) {
|
|
67
|
-
if (!htmlFile) return null;
|
|
68
|
-
const own = String(htmlFile).replace(/\.html?$/i, '.rationale.json');
|
|
69
|
-
if (isFile(own)) return own;
|
|
70
|
-
const shared = path.join(path.dirname(htmlFile), 'rationale.json');
|
|
71
|
-
return isFile(shared) ? shared : null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Read and validate a rationale file: {entries, errors:[string]}. A missing or unparseable file is one error. */
|
|
75
|
-
export function loadRationale(file) {
|
|
76
|
-
if (!file || !isFile(file)) return { entries: [], errors: ['no rationale.json beside the render source'] };
|
|
77
|
-
const doc = readJson(file);
|
|
78
|
-
if (doc === undefined) return { entries: [], errors: [`${path.basename(file)} is not JSON`] };
|
|
79
|
-
const entries = Array.isArray(doc) ? doc : null;
|
|
80
|
-
if (!entries) return { entries: [], errors: [`${path.basename(file)} is not an array of decisions`] };
|
|
81
|
-
if (!entries.length) return { entries: [], errors: [`${path.basename(file)} holds no decision`] };
|
|
82
|
-
const errors = [];
|
|
83
|
-
const seen = new Set();
|
|
84
|
-
entries.forEach((e, i) => {
|
|
85
|
-
const at = `decision ${e?.id ?? '#' + (i + 1)}`;
|
|
86
|
-
if (!e || typeof e !== 'object' || Array.isArray(e)) { errors.push(`#${i + 1} is not an object`); return; }
|
|
87
|
-
const missing = REQUIRED_FIELDS.filter((k) => e[k] == null || (typeof e[k] === 'string' && !e[k].trim()));
|
|
88
|
-
if (missing.length) errors.push(`${at} lacks ${missing.join(', ')}`);
|
|
89
|
-
if (e.id != null) { if (seen.has(String(e.id))) { errors.push(`${at}: the id is used twice`); } seen.add(String(e.id)); }
|
|
90
|
-
if (e.kind != null && !RATIONALE_KINDS.includes(e.kind)) errors.push(`${at}: kind "${e.kind}" is none of ${RATIONALE_KINDS.join('|')}`);
|
|
91
|
-
if (e.rules != null && (!Array.isArray(e.rules) || !e.rules.length)) errors.push(`${at}: rules[] must cite at least one rule id`);
|
|
92
|
-
if (e.alternativesRejected != null && (!Array.isArray(e.alternativesRejected) || e.alternativesRejected.some((a) => !a || typeof a !== 'object' || !str(a.option).trim() || !str(a.why).trim()))) {
|
|
93
|
-
errors.push(`${at}: alternativesRejected is [{option, why}]`);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
return { entries: entries.filter((e) => e && typeof e === 'object' && !Array.isArray(e)), errors };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The labels the redline shows per decision: {id: {kind, value, rule}} - rule is the first rule's id token. */
|
|
100
|
-
export function redlineLabelsOf(entries) {
|
|
101
|
-
const out = {};
|
|
102
|
-
for (const e of list(entries)) {
|
|
103
|
-
if (e?.id == null) continue;
|
|
104
|
-
out[String(e.id)] = { kind: e.kind ?? null, value: str(e.value).slice(0, 40), rule: ruleTokenOf(list(e.rules)[0]) };
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
110
|
-
// Rule id resolution
|
|
111
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
const NAMESPACES = ['knowledge', 'dna', 'grammar', 'direction', 'rubric', 'owner'];
|
|
114
|
-
|
|
115
|
-
/** The id token of a rules[] entry (namespace dropped), for labels. */
|
|
116
|
-
function ruleTokenOf(ref) {
|
|
117
|
-
const m = parseRuleRef(str(ref));
|
|
118
|
-
if (!m) return '';
|
|
119
|
-
return `${m[2]}${m[3] ? ' ' + m[3] : ''}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let knowledgeCache = null;
|
|
123
|
-
/** Every id knowledge/** declares: {ids:Set, cases:Map(ruleId -> Set(case ids))}. */
|
|
124
|
-
function addKnowledgeRecord(node, ids, cases) {
|
|
125
|
-
if (typeof node.id !== 'string' || !node.id.trim()) return;
|
|
126
|
-
const id = node.id.trim();
|
|
127
|
-
if (!/^case-\d+$/i.test(id)) ids.add(id);
|
|
128
|
-
if (!Array.isArray(node.cases)) return;
|
|
129
|
-
const set = cases.get(id) ?? new Set();
|
|
130
|
-
for (const item of node.cases) if (typeof item?.id === 'string') set.add(item.id.trim());
|
|
131
|
-
cases.set(id, set);
|
|
132
|
-
}
|
|
133
|
-
function visitKnowledgeNode(node, ids, cases) {
|
|
134
|
-
if (Array.isArray(node)) {
|
|
135
|
-
for (const item of node) visitKnowledgeNode(item, ids, cases);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (!node || typeof node !== 'object') return;
|
|
139
|
-
addKnowledgeRecord(node, ids, cases);
|
|
140
|
-
for (const value of Object.values(node)) if (value && typeof value === 'object') visitKnowledgeNode(value, ids, cases);
|
|
141
|
-
}
|
|
142
|
-
function knowledgeIndex(root = KNOWLEDGE) {
|
|
143
|
-
if (knowledgeCache?.root === root) return knowledgeCache;
|
|
144
|
-
const ids = new Set(), cases = new Map();
|
|
145
|
-
const walk = (dir) => {
|
|
146
|
-
let names = [];
|
|
147
|
-
try { names = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
148
|
-
for (const d of names) {
|
|
149
|
-
if (d.name.startsWith('.')) continue;
|
|
150
|
-
const p = path.join(dir, d.name);
|
|
151
|
-
if (d.isDirectory()) walk(p);
|
|
152
|
-
else if (/\.ya?ml$/i.test(d.name)) visitKnowledgeNode(readYamlOr(p), ids, cases);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
walk(root);
|
|
156
|
-
knowledgeCache = { root, ids, cases };
|
|
157
|
-
return knowledgeCache;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const idsUnder = (node, out = new Set()) => {
|
|
161
|
-
if (Array.isArray(node)) { for (const n of node) { idsUnder(n, out); } return out; }
|
|
162
|
-
if (!node || typeof node !== 'object') return out;
|
|
163
|
-
if (typeof node.id === 'string' && node.id.trim()) out.add(node.id.trim());
|
|
164
|
-
for (const v of Object.values(node)) if (v && typeof v === 'object') idsUnder(v, out);
|
|
165
|
-
return out;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/** The product's brand.direction: {all:Set, rubric:Set} of its ids. */
|
|
169
|
-
function directionIds(workRoot) {
|
|
170
|
-
const doc = workRoot ? readYamlOr(path.join(workRoot, 'brand', 'index.yaml')) : null;
|
|
171
|
-
const direction = doc?.brand?.direction ?? doc?.direction ?? null;
|
|
172
|
-
return { all: idsUnder(direction), rubric: new Set(list(direction?.rubric?.checks).map((c) => str(c?.id).trim()).filter(Boolean)) };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Current owner rulings, product feedback notes and actual receipts. */
|
|
176
|
-
function ownerIds({ record = null, workRoot = null, rulingsFile = OWNER_RULINGS } = {}) {
|
|
177
|
-
const ids = new Set();
|
|
178
|
-
for (const r of list(readYamlOr(rulingsFile)?.rulings)) if (r?.id) ids.add(str(r.id).trim());
|
|
179
|
-
const review = record?.ui?.review ?? {};
|
|
180
|
-
for (const n of idsUnder(review.feedback)) ids.add(n);
|
|
181
|
-
for (const r of list(review.feedback?.rounds)) if (r?.receipt) ids.add(str(r.receipt));
|
|
182
|
-
if (review.owner?.receipt) ids.add(str(review.owner.receipt));
|
|
183
|
-
if (review.owner?.dispatchId) ids.add(str(review.owner.dispatchId));
|
|
184
|
-
const doc = workRoot ? readYamlOr(path.join(workRoot, 'brand', 'index.yaml')) : null;
|
|
185
|
-
for (const l of list((doc?.brand?.direction ?? doc?.direction)?.learned)) if (l?.id) ids.add(str(l.id).trim());
|
|
186
|
-
return ids;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const kebab = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Z])([A-Z][a-z])/g, '$1-$2').toLowerCase();
|
|
190
|
-
|
|
191
|
-
/** Whether `token` names a DNA component, one of its parts, props or closed values (Button, Button.variant=secondary). */
|
|
192
|
-
function dnaResolves(dna, token) {
|
|
193
|
-
if (!dna) return false;
|
|
194
|
-
const [head, rest] = String(token).split(/\.(.+)/);
|
|
195
|
-
const spec = dna.components.get(head);
|
|
196
|
-
if (!spec) return false;
|
|
197
|
-
if (!rest) return true;
|
|
198
|
-
const [prop, value] = rest.split('=');
|
|
199
|
-
if (spec.parts.has(prop) || spec.parts.has(kebab(prop))) return value == null;
|
|
200
|
-
const closed = spec.closed.get(prop);
|
|
201
|
-
if (!closed) return false;
|
|
202
|
-
if (value == null) return true;
|
|
203
|
-
return closed.values ? closed.values.includes(value) : true;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* A resolver of rules[] entries: (ref) -> {ok, via, id, why?}. Context: {workRoot, record, dna, knowledge,
|
|
208
|
-
* rulingsFile, repoRoot}.
|
|
209
|
-
*/
|
|
210
|
-
export function ruleResolver({ workRoot = null, record = null, dna = loadDna(), knowledge = knowledgeIndex(), rulingsFile = OWNER_RULINGS, repoRoot = null } = {}) {
|
|
211
|
-
const direction = directionIds(workRoot);
|
|
212
|
-
const owner = ownerIds({ record, workRoot, rulingsFile });
|
|
213
|
-
const receiptOk = (token) => {
|
|
214
|
-
for (const base of [repoRoot, workRoot ? path.dirname(workRoot) : null, workRoot].filter(Boolean)) {
|
|
215
|
-
const f = path.resolve(base, token);
|
|
216
|
-
if (isFile(f) && readJson(f)?.schema === 'starci/ask-answer@1') return true;
|
|
217
|
-
}
|
|
218
|
-
return false;
|
|
219
|
-
};
|
|
220
|
-
const inKnowledge = (id, kase) => {
|
|
221
|
-
if (!knowledge.ids.has(id)) return { ok: false, why: `${id} is no id in knowledge/` };
|
|
222
|
-
if (kase && !(knowledge.cases.get(id)?.has(kase))) return { ok: false, why: `${id} has no ${kase}` };
|
|
223
|
-
return { ok: true };
|
|
224
|
-
};
|
|
225
|
-
const resolveNamespace = (namespace, token, kase) => {
|
|
226
|
-
if (namespace === 'knowledge') {
|
|
227
|
-
const result = inKnowledge(token, kase);
|
|
228
|
-
return result.ok ? { ok: true, via: 'knowledge', id: token } : { via: 'knowledge', why: result.why };
|
|
229
|
-
}
|
|
230
|
-
if (namespace === 'dna' && dnaResolves(dna, token)) return { ok: true, via: 'dna', id: token };
|
|
231
|
-
if (namespace === 'direction' && direction.all.has(token)) return { ok: true, via: 'direction', id: token };
|
|
232
|
-
if (namespace === 'rubric' && direction.rubric.has(token)) return { ok: true, via: 'rubric', id: token };
|
|
233
|
-
if (namespace === 'owner' && (owner.has(token) || receiptOk(token))) return { ok: true, via: 'owner', id: token };
|
|
234
|
-
return null;
|
|
235
|
-
};
|
|
236
|
-
return (ref) => {
|
|
237
|
-
const text = str(ref);
|
|
238
|
-
const m = parseRuleRef(text);
|
|
239
|
-
if (!m?.[2]) return { ok: false, id: text, why: 'empty rule reference' };
|
|
240
|
-
const ns = m[1] ? m[1].toLowerCase() : null;
|
|
241
|
-
const token = bareRuleToken(m[2]);
|
|
242
|
-
const kase = m[3] ?? null;
|
|
243
|
-
if (ns && !NAMESPACES.includes(ns)) return { ok: false, id: token, why: `unknown namespace ${ns}: (one of ${NAMESPACES.join(', ')})` };
|
|
244
|
-
const tries = ns ? [ns === 'grammar' ? 'dna' : ns] : ['knowledge', 'dna', 'direction', 'owner'];
|
|
245
|
-
let why = null;
|
|
246
|
-
for (const t of tries) {
|
|
247
|
-
const result = resolveNamespace(t, token, kase);
|
|
248
|
-
if (result?.ok) return result;
|
|
249
|
-
if (result?.via === 'knowledge') why ??= result.why;
|
|
250
|
-
}
|
|
251
|
-
return { ok: false, id: token, why: ns ? `${ns}:${token} resolves to nothing` : (why ?? `${token} resolves to no knowledge id, DNA name, brand.direction id or owner ruling`) };
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
256
|
-
// Numbers stated in a decision
|
|
257
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
/** The numbers a decision states, in px where a unit says so (1.5rem -> 24); pill/full/circle as words. */
|
|
260
|
-
export function statedValues(text) {
|
|
261
|
-
const s = str(text);
|
|
262
|
-
const nums = new Set();
|
|
263
|
-
for (const m of s.matchAll(/(\d+(?:\.\d+)?)\s*(rem|em|px|%)?/gi)) {
|
|
264
|
-
const n = Number(m[1]);
|
|
265
|
-
if (!Number.isFinite(n)) continue;
|
|
266
|
-
const unit = (m[2] ?? '').toLowerCase();
|
|
267
|
-
if (unit === 'rem' || unit === 'em') nums.add(round(n * 16));
|
|
268
|
-
else nums.add(round(n));
|
|
269
|
-
}
|
|
270
|
-
return { nums, pill: /\b(pill|full|9999|rounded-full|capsule)\b/i.test(s), circle: /\b(circle|50%)\b/i.test(s) };
|
|
271
|
-
}
|
|
272
|
-
const round = (n) => Math.round(Number(n) * 100) / 100;
|
|
273
|
-
|
|
274
|
-
const statesValue = (entry, v) => {
|
|
275
|
-
const st = statedValues(`${str(entry.value)}`);
|
|
276
|
-
if (typeof v === 'string') {
|
|
277
|
-
if (v === 'pill') return st.pill;
|
|
278
|
-
if (v === 'circle') return st.circle;
|
|
279
|
-
return str(entry.value).toLowerCase().includes(v.toLowerCase());
|
|
280
|
-
}
|
|
281
|
-
return [...st.nums].some((n) => Math.abs(n - v) < 0.26);
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
/** The id-and-reference test of `because`: a record ref, an FR/NFR/BR id, the content or a user job. */
|
|
285
|
-
export function becauseCites(because, refs = []) {
|
|
286
|
-
const s = str(because);
|
|
287
|
-
if (!s.trim()) return false;
|
|
288
|
-
if (refs.some((r) => r && s.includes(r))) return true;
|
|
289
|
-
return [
|
|
290
|
-
/\b(?:N?FR|BR|UJ|JTBD|US)[-.]?[a-z0-9]*\d/i,
|
|
291
|
-
/\bfr\.[a-z0-9][\w.-]*/i,
|
|
292
|
-
/\b\d{2}-CONTENT\b/i,
|
|
293
|
-
/\bcontent\b/i,
|
|
294
|
-
/\buser(?:'s)? job\b/i,
|
|
295
|
-
/\bjob[- ]to[- ]be[- ]done\b/i,
|
|
296
|
-
/\bjob:/i,
|
|
297
|
-
].some((pattern) => pattern.test(s));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** The region container of a render: the PageContainer root, else <main>, else <body>. */
|
|
301
|
-
export function regionContainerOf(tree) {
|
|
302
|
-
const all = walkElements(tree);
|
|
303
|
-
return all.find((e) => str(e.attrs[COMPONENT_ATTR]).trim() === 'PageContainer') ?? all.find((e) => e.tag === 'main') ?? all.find((e) => e.tag === 'body') ?? tree;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* DRAW_RATIONALE_MISSING findings of one render source. `html` its text; `entries`/`errors` from loadRationale;
|
|
308
|
-
* `measures` the draw-render measurements (measureRationale) of every viewport it was rendered at; `resolve` a
|
|
309
|
-
* ruleResolver; `record` the ui record (its refs); `redlines` [{viewport, ok}] when the caller checks the redline
|
|
310
|
-
* renders. Returns [{code, kind, detail, count, examples}].
|
|
311
|
-
*/
|
|
312
|
-
export function rationaleFindings({ html, entries = [], errors = [], measures = [], resolve = ruleResolver(), record = null, dna = loadDna(), label = 'the render', redlines = null } = {}) {
|
|
313
|
-
return findRationaleIssues({ html, entries, errors, measures, resolve, record, dna, label, redlines }, {
|
|
314
|
-
str, statesValue, round, statedValues, becauseCites, kebab, regionContainerOf,
|
|
315
|
-
code: DRAW_RATIONALE_MISSING, measureSchema: MEASURE_SCHEMA, whyAttr: WHY_ATTR, redlineAttr: REDLINE_ATTR,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
/** The measures of a render source's parts: the rationale block of each starci/draw-render@1 record given. */
|
|
319
|
-
export const measuresOf = (records) => list(records).map((r) => r?.rationale ?? null).filter(Boolean);
|
|
320
|
-
|
|
321
|
-
/** A one-line summary of a rationale file for the owner's ask: {decisions, byKind}. */
|
|
322
|
-
export function rationaleSummary(file) {
|
|
323
|
-
const { entries } = loadRationale(file);
|
|
324
|
-
const byKind = {};
|
|
325
|
-
for (const e of entries) byKind[e.kind] = (byKind[e.kind] ?? 0) + 1;
|
|
326
|
-
return { decisions: entries.length, byKind };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
330
|
-
// CLI
|
|
331
|
-
// ---------------------------------------------------------------------------------------------------------
|
|
332
|
-
|
|
333
|
-
async function main(argv) {
|
|
334
|
-
const file = argv.find((a) => !a.startsWith('--') && /\.html?$/i.test(a));
|
|
335
|
-
const at = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : null; };
|
|
336
|
-
if (!file) { process.stderr.write('use: starci work draw-rationale <render.html> [--rationale <file>] [--ui <ui-record-dir>] [--records <draw-render.json>[,...]] [--json]\n'); return 2; }
|
|
337
|
-
const html = path.resolve(file);
|
|
338
|
-
const rationale = at('--rationale') ? path.resolve(at('--rationale')) : rationaleFileOf(html);
|
|
339
|
-
const { entries, errors } = loadRationale(rationale);
|
|
340
|
-
const uiDir = at('--ui') ? path.resolve(at('--ui')) : null;
|
|
341
|
-
const record = uiDir ? readYamlOr(path.join(uiDir, 'index.yaml')) : null;
|
|
342
|
-
let workRoot = null;
|
|
343
|
-
for (let d = uiDir ?? path.dirname(html); ; d = path.dirname(d)) { if (path.basename(d) === '.starciwork') { workRoot = d; break; } if (path.dirname(d) === d) break; }
|
|
344
|
-
const records = (at('--records') ? at('--records').split(',') : []).map((f) => readJson(path.resolve(f)));
|
|
345
|
-
const findings = rationaleFindings({ html: fs.readFileSync(html, 'utf8'), entries, errors, measures: measuresOf(records), resolve: ruleResolver({ workRoot, record, repoRoot: process.cwd() }), record, label: path.basename(html) });
|
|
346
|
-
if (argv.includes('--json')) process.stdout.write(`${JSON.stringify({ ok: !findings.length, rationale, decisions: entries.length, findings }, null, 2)}\n`);
|
|
347
|
-
else process.stdout.write(`${findings.length ? 'REFUSED' : 'ok'}: ${entries.length} decision(s), ${findings.length} finding group(s)\n${findings.map((f) => ' [' + f.code + '] ' + f.detail).join('\n')}\n`);
|
|
348
|
-
return findings.length ? 1 : 0;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (isMain(import.meta.url)) {
|
|
352
|
-
process.exitCode = await main(process.argv.slice(2));
|
|
353
|
-
}
|