@skillstech/thunderlang 0.1.6
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 +375 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/core.cjs +6768 -0
- package/dist/index.cjs +9308 -0
- package/intent-graph.schema.json +687 -0
- package/package.json +84 -0
- package/src/ai-core.mjs +67 -0
- package/src/ai-events.mjs +56 -0
- package/src/ai.mjs +324 -0
- package/src/arch.mjs +55 -0
- package/src/atlas.mjs +118 -0
- package/src/changes.mjs +74 -0
- package/src/classification.mjs +36 -0
- package/src/cli.mjs +1534 -0
- package/src/codegen.mjs +214 -0
- package/src/compile.mjs +142 -0
- package/src/comprehension.mjs +130 -0
- package/src/conflict.mjs +0 -0
- package/src/core.d.ts +99 -0
- package/src/core.mjs +92 -0
- package/src/data-schema.mjs +137 -0
- package/src/decision.mjs +38 -0
- package/src/distributed.mjs +48 -0
- package/src/draft.mjs +101 -0
- package/src/drift.mjs +177 -0
- package/src/emit.mjs +519 -0
- package/src/exporters.mjs +245 -0
- package/src/expr.mjs +245 -0
- package/src/fable.mjs +110 -0
- package/src/focus.mjs +151 -0
- package/src/format.mjs +55 -0
- package/src/governance.mjs +100 -0
- package/src/graph-source.mjs +292 -0
- package/src/guard.mjs +105 -0
- package/src/guardian.mjs +98 -0
- package/src/hash.mjs +89 -0
- package/src/importers.mjs +194 -0
- package/src/index.d.ts +725 -0
- package/src/index.mjs +185 -0
- package/src/intellisense.mjs +210 -0
- package/src/intent-atlas.mjs +77 -0
- package/src/intent-graph.mjs +346 -0
- package/src/intent-ir.mjs +144 -0
- package/src/intent-schema.mjs +215 -0
- package/src/ledger.mjs +109 -0
- package/src/lifecycle.mjs +56 -0
- package/src/lift.mjs +693 -0
- package/src/lsp.mjs +152 -0
- package/src/mcp.mjs +158 -0
- package/src/migrate.mjs +118 -0
- package/src/outcome.mjs +93 -0
- package/src/parse.mjs +733 -0
- package/src/patch.mjs +364 -0
- package/src/privacy.mjs +61 -0
- package/src/proof-schema.mjs +129 -0
- package/src/report.mjs +84 -0
- package/src/runtime.mjs +96 -0
- package/src/sarif.mjs +88 -0
- package/src/scan-queries.mjs +97 -0
- package/src/scan.mjs +87 -0
- package/src/security.mjs +73 -0
- package/src/select.mjs +80 -0
- package/src/semantic-diff.mjs +125 -0
- package/src/simulate.mjs +106 -0
- package/src/style.mjs +250 -0
- package/src/sync.mjs +103 -0
- package/src/testing.mjs +59 -0
- package/src/twelve-factor.mjs +173 -0
- package/src/verify-diff.mjs +105 -0
- package/src/xml.mjs +87 -0
- package/syntaxes/intent.tmLanguage.json +55 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Canonical Intent Graph schema (intent-graph-v1) , the single source consumers
|
|
2
|
+
// generate bindings from, so nobody hand-recreates node types / relationship types /
|
|
3
|
+
// enums / diagnostics (gap-closure program non-negotiable #1). Pure (no Node deps).
|
|
4
|
+
//
|
|
5
|
+
// IL owns + versions this. OT/RM/ST/STW import these enums + the JSON Schema instead
|
|
6
|
+
// of maintaining their own copies. `intent schema` emits the JSON Schema.
|
|
7
|
+
|
|
8
|
+
import { CLASSIFICATIONS, CONFIDENCE } from './classification.mjs';
|
|
9
|
+
|
|
10
|
+
export const SCHEMA_VERSION = 'intent-graph-v1';
|
|
11
|
+
|
|
12
|
+
// Canonical node types (superset; Phase-1 emits a subset).
|
|
13
|
+
export const NODE_TYPES = [
|
|
14
|
+
'Mission', 'Actor', 'Persona', 'Evidence', 'Opportunity', 'Outcome', 'Metric',
|
|
15
|
+
'Requirement', 'Constraint', 'Invariant', 'Guarantee', 'Never', 'Conflict', 'Journey',
|
|
16
|
+
'ExperienceContract', 'ExperienceState', 'Pattern', 'DesignArtifact', 'DesignComponent',
|
|
17
|
+
'StyleIntent', 'Capability', 'SystemContract', 'ImplementationMapping', 'VerificationRule',
|
|
18
|
+
'VerificationResult', 'Approval', 'Release', 'OutcomeContract', 'OutcomeResult', 'LearningArtifact',
|
|
19
|
+
'Unknown', 'Assumption', 'Question', 'Lifecycle', 'LifecycleState', 'Temporal',
|
|
20
|
+
'Command', 'Event', 'FailureHandler', 'Decision', 'Rule', 'Skill',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Canonical relationship types.
|
|
24
|
+
export const RELATIONSHIP_TYPES = [
|
|
25
|
+
'supported_by', 'derived_from', 'addresses', 'targets', 'measured_by', 'requires',
|
|
26
|
+
'constrained_by', 'implemented_by', 'represented_by', 'verified_by', 'approved_by',
|
|
27
|
+
'released_in', 'resulted_in', 'contradicts', 'supersedes', 'depends_on', 'blocks',
|
|
28
|
+
'teaches', 'generated_from', 'transitions_to', 'requires_skill',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const NODE_STATUSES = [
|
|
32
|
+
'draft', 'proposed', 'approval-required', 'unresolved', 'unvalidated', 'open',
|
|
33
|
+
'required', 'verify-declared', 'unverified', 'verified', 'recoverable', 'defined', 'redundant',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** The canonical JSON Schema (draft-07) for an intent-graph-v1 document. */
|
|
37
|
+
export function intentGraphJsonSchema() {
|
|
38
|
+
return {
|
|
39
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
40
|
+
$id: `https://thunderlang.dev/schema/${SCHEMA_VERSION}.json`,
|
|
41
|
+
title: 'Intent Graph',
|
|
42
|
+
type: 'object',
|
|
43
|
+
required: ['schema', 'missionId', 'nodes', 'relationships'],
|
|
44
|
+
properties: {
|
|
45
|
+
schema: { const: SCHEMA_VERSION },
|
|
46
|
+
missionId: { type: 'string' },
|
|
47
|
+
nodes: { type: 'array', items: { $ref: '#/definitions/node' } },
|
|
48
|
+
relationships: { type: 'array', items: { $ref: '#/definitions/relationship' } },
|
|
49
|
+
summary: { type: 'object' },
|
|
50
|
+
},
|
|
51
|
+
definitions: {
|
|
52
|
+
node: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
required: ['id', 'type'],
|
|
55
|
+
properties: {
|
|
56
|
+
id: { type: 'string' },
|
|
57
|
+
type: { enum: NODE_TYPES },
|
|
58
|
+
title: { type: ['string', 'null'] },
|
|
59
|
+
description: { type: ['string', 'null'] },
|
|
60
|
+
status: { type: ['string', 'null'] },
|
|
61
|
+
owner: { type: ['string', 'null'] },
|
|
62
|
+
classification: { type: ['string', 'null'], enum: [...CLASSIFICATIONS, null] },
|
|
63
|
+
confidence: { type: ['string', 'null'], enum: [...CONFIDENCE, null] },
|
|
64
|
+
source: { type: ['string', 'null'] },
|
|
65
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
66
|
+
createdTime: { type: ['string', 'null'] },
|
|
67
|
+
updatedTime: { type: ['string', 'null'] },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
relationship: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
required: ['from', 'type', 'to'],
|
|
73
|
+
properties: {
|
|
74
|
+
from: { type: 'string' },
|
|
75
|
+
type: { enum: RELATIONSHIP_TYPES },
|
|
76
|
+
to: { type: 'string' },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Canonical diagnostic-rule catalog , stable IDs so every product uses the same ones.
|
|
84
|
+
const IL_AUTHOR_RULE_ROWS = [
|
|
85
|
+
{ ruleId: 'IL-PM-001', area: 'product', severity: 'warning', blocks: ['release'], summary: 'Metric has no measurement window.' },
|
|
86
|
+
{ ruleId: 'IL-PM-003', area: 'product', severity: 'warning', blocks: [], summary: 'Outcome has no metric.' },
|
|
87
|
+
{ ruleId: 'IL-EV-001', area: 'evidence', severity: 'info', blocks: [], summary: 'Evidence has no classification.' },
|
|
88
|
+
{ ruleId: 'IL-EV-002', area: 'evidence', severity: 'warning', blocks: [], summary: 'Evidence has an unknown classification.' },
|
|
89
|
+
{ ruleId: 'IL-GRAPH-010', area: 'graph', severity: 'blocker', blocks: ['<declared-phase>'], summary: 'Unresolved unknown blocks a phase.' },
|
|
90
|
+
{ ruleId: 'IL-GRAPH-011', area: 'graph', severity: 'blocker', blocks: ['<declared-phase>'], summary: 'Open question blocks a phase.' },
|
|
91
|
+
{ ruleId: 'IL-EXP-001', area: 'experience', severity: 'info', blocks: [], summary: 'Experience declares no states.' },
|
|
92
|
+
{ ruleId: 'IL-EXP-004', area: 'experience', severity: 'blocker', blocks: ['experience-approval', 'release'], summary: 'Failure state has no recovery path.' },
|
|
93
|
+
{ ruleId: 'IL-CONFLICT-001', area: 'conflict', severity: 'blocker', blocks: ['<declared-phase>'], summary: 'Declared conflict is unresolved.' },
|
|
94
|
+
{ ruleId: 'IL-CONFLICT-010', area: 'conflict', severity: 'blocker', blocks: ['implementation'], summary: 'Scope includes and excludes the same item.' },
|
|
95
|
+
{ ruleId: 'IL-CONFLICT-011', area: 'conflict', severity: 'info', blocks: [], summary: 'Redundant constraint from multiple roles.' },
|
|
96
|
+
{ ruleId: 'IL-CONFLICT-012', area: 'conflict', severity: 'blocker', blocks: ['implementation'], summary: 'Directly contradictory constraints.' },
|
|
97
|
+
{ ruleId: 'IL-GOV-001', area: 'governance', severity: 'error', blocks: [], summary: 'Waiver names no diagnostic code.' },
|
|
98
|
+
{ ruleId: 'IL-GOV-002', area: 'governance', severity: 'error', blocks: [], summary: 'Waiver has no reason.' },
|
|
99
|
+
{ ruleId: 'IL-GOV-003', area: 'governance', severity: 'error', blocks: [], summary: 'Waiver names no approver.' },
|
|
100
|
+
{ ruleId: 'IL-GOV-004', area: 'governance', severity: 'warning', blocks: [], summary: 'Waiver matches no current diagnostic (stale).' },
|
|
101
|
+
{ ruleId: 'IL-GOV-005', area: 'governance', severity: 'error', blocks: [], summary: 'Waiver has expired.' },
|
|
102
|
+
{ ruleId: 'IL-DATA-001', area: 'privacy', severity: 'blocker', blocks: ['release'], summary: 'Sensitive data held with no stated purpose.' },
|
|
103
|
+
{ ruleId: 'IL-DATA-002', area: 'privacy', severity: 'warning', blocks: [], summary: 'Sensitive data has no retention rule.' },
|
|
104
|
+
{ ruleId: 'IL-DATA-003', area: 'privacy', severity: 'blocker', blocks: ['release'], summary: 'Sensitive data declares no lawful basis.' },
|
|
105
|
+
{ ruleId: 'IL-DATA-004', area: 'privacy', severity: 'warning', blocks: [], summary: 'Data has an unknown classification.' },
|
|
106
|
+
{ ruleId: 'IL-DATA-005', area: 'privacy', severity: 'warning', blocks: [], summary: 'Unrecognized lawful basis.' },
|
|
107
|
+
{ ruleId: 'IL-DATA-006', area: 'privacy', severity: 'warning', blocks: [], summary: 'Sensitive data exposed as output with no guard.' },
|
|
108
|
+
{ ruleId: 'IL-OC-001', area: 'outcome', severity: 'blocker', blocks: ['release'], summary: 'Outcome contract has no target (cannot be evaluated).' },
|
|
109
|
+
{ ruleId: 'IL-OC-002', area: 'outcome', severity: 'warning', blocks: [], summary: 'Outcome contract names no metric.' },
|
|
110
|
+
{ ruleId: 'IL-OC-003', area: 'outcome', severity: 'blocker', blocks: ['release'], summary: 'Outcome contract has no measurement window.' },
|
|
111
|
+
{ ruleId: 'IL-OC-004', area: 'outcome', severity: 'warning', blocks: [], summary: 'Outcome contract target is not better than its baseline.' },
|
|
112
|
+
{ ruleId: 'IL-OC-005', area: 'outcome', severity: 'warning', blocks: [], summary: 'Outcome contract has a target but no guardrails (the target is gameable).' },
|
|
113
|
+
{ ruleId: 'IL-OC-006', area: 'outcome', severity: 'info', blocks: [], summary: 'Outcome contract has no attribution (correlation is not causation).' },
|
|
114
|
+
{ ruleId: 'IL-DEC-001', area: 'decision', severity: 'blocker', blocks: ['implementation'], summary: 'Decision has no default (undefined when no rule matches).' },
|
|
115
|
+
{ ruleId: 'IL-DEC-002', area: 'decision', severity: 'blocker', blocks: ['implementation'], summary: 'Two rules fire on the same condition with different results.' },
|
|
116
|
+
{ ruleId: 'IL-DEC-003', area: 'decision', severity: 'info', blocks: [], summary: 'Redundant rule, identical to another (dead).' },
|
|
117
|
+
{ ruleId: 'IL-DEC-004', area: 'decision', severity: 'warning', blocks: [], summary: 'Decision has no rules.' },
|
|
118
|
+
{ ruleId: 'IL-DIST-001', area: 'distributed', severity: 'blocker', blocks: ['implementation'], summary: 'Retry without an idempotency key (duplicates work).' },
|
|
119
|
+
{ ruleId: 'IL-DIST-002', area: 'distributed', severity: 'blocker', blocks: ['implementation'], summary: 'Retried or remote command with no timeout.' },
|
|
120
|
+
{ ruleId: 'IL-DIST-003', area: 'distributed', severity: 'blocker', blocks: ['implementation'], summary: 'At-least-once delivery without duplicate handling.' },
|
|
121
|
+
{ ruleId: 'IL-DIST-004', area: 'distributed', severity: 'blocker', blocks: ['implementation'], summary: 'Permanent failure with no compensation.' },
|
|
122
|
+
{ ruleId: 'IL-DIST-005', area: 'distributed', severity: 'error', blocks: [], summary: 'Handler references an undeclared event.' },
|
|
123
|
+
{ ruleId: 'IL-LIFE-001', area: 'lifecycle', severity: 'error', blocks: [], summary: 'Transition references an undefined state.' },
|
|
124
|
+
{ ruleId: 'IL-LIFE-002', area: 'lifecycle', severity: 'warning', blocks: [], summary: 'Unreachable state.' },
|
|
125
|
+
{ ruleId: 'IL-LIFE-003', area: 'lifecycle', severity: 'warning', blocks: [], summary: 'Non-terminal dead-end state (no way out).' },
|
|
126
|
+
{ ruleId: 'IL-LIFE-004', area: 'lifecycle', severity: 'warning', blocks: [], summary: 'Lifecycle has no initial state.' },
|
|
127
|
+
{ ruleId: 'IL-TEMP-001', area: 'temporal', severity: 'blocker', blocks: ['verification'], summary: 'Eventually with no time bound (cannot be verified).' },
|
|
128
|
+
{ ruleId: 'IL-STYLE-001', area: 'style', severity: 'warning', blocks: [], summary: 'Style intent binds a token outside the canonical address space.' },
|
|
129
|
+
{ ruleId: 'IL-STYLE-002', area: 'style', severity: 'warning', blocks: [], summary: 'Unrecognized accessibility target.' },
|
|
130
|
+
{ ruleId: 'IL-STYLE-003', area: 'style', severity: 'info', blocks: [], summary: 'Style intent declares no accessibility target (a proposed claim).' },
|
|
131
|
+
{ ruleId: 'IL-STYLE-004', area: 'style', severity: 'warning', blocks: [], summary: 'Invalid mode token value.' },
|
|
132
|
+
{ ruleId: 'IL-STYLE-005', area: 'style', severity: 'info', blocks: [], summary: 'Style intent applies to an undeclared experience.' },
|
|
133
|
+
{ ruleId: 'IL-SEC-001', area: 'security', severity: 'blocker', blocks: ['release'], summary: 'Secret-typed field travels over the event bus.' },
|
|
134
|
+
{ ruleId: 'IL-SEC-002', area: 'security', severity: 'blocker', blocks: ['release'], summary: 'API returns a secret with no auth requirement.' },
|
|
135
|
+
{ ruleId: 'IL-TYPE-001', area: 'type', severity: 'info', blocks: [], summary: 'Field uses an unrecognized (likely mistyped) type.' },
|
|
136
|
+
// 12-Factor Agents conformance (twelve-factor-v1). Advisory: an intent's alignment with the
|
|
137
|
+
// humanlayer/12-factor-agents principles. `intent twelve-factor` scores these.
|
|
138
|
+
{ ruleId: 'IL-12F-01', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F1 Natural language to tool calls: model dispatch as a decision/command.' },
|
|
139
|
+
{ ruleId: 'IL-12F-02', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F2 Own your prompts: behavior is an owned, guaranteed contract, not a black box.' },
|
|
140
|
+
{ ruleId: 'IL-12F-03', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F3 Own your context window: declare a scope boundary.' },
|
|
141
|
+
{ ruleId: 'IL-12F-04', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F4 Tools are structured outputs: typed I/O + discriminated results.' },
|
|
142
|
+
{ ruleId: 'IL-12F-05', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F5 Unify execution + business state: one lifecycle/state model.' },
|
|
143
|
+
{ ruleId: 'IL-12F-06', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F6 Launch/pause/resume: a resumable lifecycle with waiting + terminal states.' },
|
|
144
|
+
{ ruleId: 'IL-12F-07', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F7 Contact humans with tool calls: a structured approval/human-input gate.' },
|
|
145
|
+
{ ruleId: 'IL-12F-08', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F8 Own your control flow: decisions with an explicit default (total control flow).' },
|
|
146
|
+
{ ruleId: 'IL-12F-09', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F9 Compact errors into context: named errors + handlers (bounded retry).' },
|
|
147
|
+
{ ruleId: 'IL-12F-10', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F10 Small, focused agents: keep operations within ~10 steps (20 max).' },
|
|
148
|
+
{ ruleId: 'IL-12F-11', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F11 Trigger from anywhere: declare event triggers.' },
|
|
149
|
+
{ ruleId: 'IL-12F-12', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F12 Stateless reducer: pure, replayable decision/lifecycle logic.' },
|
|
150
|
+
{ ruleId: 'IL-12F-13', area: 'twelve-factor', severity: 'info', blocks: [], summary: 'F13 Pre-fetch context: declare inputs so known data is fetched up front.' },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Core check-surface diagnostics that predate the IL-* catalog. They keep their stable legacy
|
|
154
|
+
// IDs (renaming would break consumers keyed on them), so they live here rather than in
|
|
155
|
+
// DIAGNOSTIC_RULES (which is IL-* only). Documented + explainable all the same; consumers who
|
|
156
|
+
// want the full set use ALL_DIAGNOSTICS below.
|
|
157
|
+
const IL_CORE_RULE_ROWS = [
|
|
158
|
+
{ ruleId: 'missing-goal', area: 'core', severity: 'warning', blocks: [], summary: 'Mission has no goal block.' },
|
|
159
|
+
{ ruleId: 'duplicate-without-idempotency', area: 'core', severity: 'warning', blocks: [], summary: 'A duplicate-prevention guarantee declares no idempotency key or lookup rule.' },
|
|
160
|
+
{ ruleId: 'guarantee-without-verification', area: 'core', severity: 'warning', blocks: [], summary: 'Guarantee has no explicit verification.' },
|
|
161
|
+
{ ruleId: 'never-without-verification', area: 'core', severity: 'warning', blocks: [], summary: 'Never-rule has no explicit verification.' },
|
|
162
|
+
{ ruleId: 'invariant-without-verification', area: 'core', severity: 'warning', blocks: [], summary: 'Global invariant has no explicit verification.' },
|
|
163
|
+
{ ruleId: 'secret-without-never-log', area: 'core', severity: 'warning', blocks: [], summary: 'A secret field has no "never log/expose" guard.' },
|
|
164
|
+
{ ruleId: 'error-name-not-pascalcase', area: 'core', severity: 'info', blocks: [], summary: 'Named error is not PascalCase.' },
|
|
165
|
+
{ ruleId: 'unknown-block', area: 'core', severity: 'info', blocks: [], summary: 'Unrecognized top-level block.' },
|
|
166
|
+
{ ruleId: 'INTENT-ARCH-001', area: 'architecture', severity: 'warning', blocks: [], summary: 'Architecture rule not understood.' },
|
|
167
|
+
{ ruleId: 'INTENT-AI-010', area: 'ai', severity: 'warning', blocks: [], summary: 'Unsupported implementation scope.' },
|
|
168
|
+
{ ruleId: 'INTENT_NOTE_UNKNOWN_LENS', area: 'note', severity: 'info', blocks: [], summary: 'IntentLens note uses an unknown lens.' },
|
|
169
|
+
{ ruleId: 'INTENT_NOTE_EMPTY', area: 'note', severity: 'info', blocks: [], summary: 'IntentLens note is empty.' },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
// ── Rule namespaces: ONE id space across two PHASES ──────────────────────────
|
|
173
|
+
// The canonical catalog spans author-time (IL) and verify-time (OpenThunder). Every rule row
|
|
174
|
+
// self-describes with `owner` ('IL' | 'OT') and `phase` ('author' | 'verify'), and prefix
|
|
175
|
+
// ownership is declared in RULE_NAMESPACES, so a reader (or a coverage/diff lens) sees a single,
|
|
176
|
+
// unambiguous rule catalog instead of two forks. IL owns author-time rules (checked when the
|
|
177
|
+
// .intent is authored/compiled); OT owns verify-time rules (checked against the actual repo).
|
|
178
|
+
// Additive: existing rows gain owner/phase; existing consumers keying on ruleId are unaffected.
|
|
179
|
+
export const RULE_PHASES = ['author', 'verify'];
|
|
180
|
+
export const RULE_OWNERS = ['IL', 'OT'];
|
|
181
|
+
export const RULE_NAMESPACES = [
|
|
182
|
+
{ prefix: 'IL-', owner: 'IL', phase: 'author', description: 'ThunderLang author-time catalog (compile-time).' },
|
|
183
|
+
{ prefix: 'OT-', owner: 'OT', phase: 'verify', description: 'OpenThunder verify-time rules (repo vs intent). OT-owned; OT PRs the rows.' },
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const stamp = (owner, phase) => (r) => ({ owner, phase, ...r });
|
|
187
|
+
|
|
188
|
+
// IL author-time catalog (IL-* canonical + legacy core ids), stamped IL/author.
|
|
189
|
+
export const DIAGNOSTIC_RULES = IL_AUTHOR_RULE_ROWS.map(stamp('IL', 'author'));
|
|
190
|
+
export const CORE_DIAGNOSTICS = IL_CORE_RULE_ROWS.map(stamp('IL', 'author'));
|
|
191
|
+
|
|
192
|
+
// OpenThunder verify-time namespace , RESERVED here so the id space is single-source; OT PRs the
|
|
193
|
+
// row definitions. `reserved: true` marks a slot that OT owns and has not yet defined, so a
|
|
194
|
+
// coverage/diff lens can see the id today without IL fabricating OT's rule semantics.
|
|
195
|
+
export const VERIFICATION_RULES = [
|
|
196
|
+
'OT-REQ-001', 'OT-REQ-002', 'OT-REQ-003', 'OT-REQ-004', 'OT-REQ-005', 'OT-REQ-006',
|
|
197
|
+
].map((ruleId) => ({
|
|
198
|
+
ruleId, area: 'verification', severity: 'error', blocks: ['<verify-time>'],
|
|
199
|
+
summary: 'Reserved for an OpenThunder verify-time rule; OT owns the definition.',
|
|
200
|
+
owner: 'OT', phase: 'verify', reserved: true,
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
// The full set of documented rules: IL author-time catalog + legacy core codes + the reserved
|
|
204
|
+
// OT verify-time namespace , one id space across both phases.
|
|
205
|
+
export const ALL_DIAGNOSTICS = [...DIAGNOSTIC_RULES, ...CORE_DIAGNOSTICS, ...VERIFICATION_RULES];
|
|
206
|
+
|
|
207
|
+
// Resolve which namespace (owner + phase) a rule id belongs to. Prefix-based for IL-/OT-, with the
|
|
208
|
+
// legacy core ids (no IL-/OT- prefix) explicitly classified IL/author. Unknown ids return null.
|
|
209
|
+
const IL_CORE_IDS = new Set(IL_CORE_RULE_ROWS.map((r) => r.ruleId));
|
|
210
|
+
export function ruleNamespace(ruleId) {
|
|
211
|
+
const id = String(ruleId || '');
|
|
212
|
+
for (const ns of RULE_NAMESPACES) if (id.startsWith(ns.prefix)) return { owner: ns.owner, phase: ns.phase };
|
|
213
|
+
if (IL_CORE_IDS.has(id)) return { owner: 'IL', phase: 'author' };
|
|
214
|
+
return null;
|
|
215
|
+
}
|
package/src/ledger.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Intent Ledger (intent-ledger-v1) , the append-only, tamper-evident record of MEANING and history
|
|
2
|
+
// for a project: provenance, decisions, assumptions, approvals, rejections, corrections, candidate
|
|
3
|
+
// intent, confidence changes, evidence, findings, risk-acceptance, implementation hashes,
|
|
4
|
+
// verification results, lesson versions, ownership, and change. It answers the questions a project
|
|
5
|
+
// loses over time: why was this built, who approved it, what assumptions were made, which code
|
|
6
|
+
// implements it, which tests prove it, what changed, which lessons went stale, which risks were
|
|
7
|
+
// accepted, which inferred intent was corrected.
|
|
8
|
+
//
|
|
9
|
+
// Every entry is hash-chained (each hashes over the previous), so the ledger is tamper-evident ,
|
|
10
|
+
// you cannot quietly rewrite history. Deterministic; the caller supplies timestamps so the ledger
|
|
11
|
+
// is reproducible and testable.
|
|
12
|
+
|
|
13
|
+
import { sha256 } from './emit.mjs';
|
|
14
|
+
|
|
15
|
+
export const LEDGER_SCHEMA = 'intent-ledger-v1';
|
|
16
|
+
|
|
17
|
+
export const ENTRY_TYPES = [
|
|
18
|
+
'intent-version', 'decision', 'assumption', 'approval', 'rejection', 'correction',
|
|
19
|
+
'candidate-intent', 'confidence-change', 'evidence', 'finding', 'risk-acceptance',
|
|
20
|
+
'implementation-hash', 'verification-result', 'lesson-version', 'ownership', 'change',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function emptyLedger() {
|
|
24
|
+
return { schema: LEDGER_SCHEMA, entries: [], head: null };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Canonical, order-stable payload so the hash is reproducible.
|
|
28
|
+
function payloadOf(seq, entry, prev) {
|
|
29
|
+
return {
|
|
30
|
+
seq,
|
|
31
|
+
type: entry.type,
|
|
32
|
+
subject: entry.subject ?? null,
|
|
33
|
+
actor: entry.actor ?? null,
|
|
34
|
+
at: entry.at ?? null,
|
|
35
|
+
note: entry.note ?? null,
|
|
36
|
+
data: entry.data ?? {},
|
|
37
|
+
prev,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Append an entry to the ledger, hash-chained over the previous head. Returns a NEW ledger. */
|
|
42
|
+
export function record(ledger, entry) {
|
|
43
|
+
if (!entry || !ENTRY_TYPES.includes(entry.type)) throw new Error(`intent ledger: unknown entry type "${entry?.type}"`);
|
|
44
|
+
const base = ledger && Array.isArray(ledger.entries) ? ledger : emptyLedger();
|
|
45
|
+
const payload = payloadOf(base.entries.length, entry, base.head);
|
|
46
|
+
const hash = sha256(JSON.stringify(payload));
|
|
47
|
+
const rec = { ...payload, hash };
|
|
48
|
+
return { ...base, entries: [...base.entries, rec], head: hash };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Record several entries in order. */
|
|
52
|
+
export function recordAll(ledger, entries) {
|
|
53
|
+
return (entries || []).reduce((l, e) => record(l, e), ledger || emptyLedger());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Verify the hash chain , tamper-evidence. Returns { valid, brokenAt }. */
|
|
57
|
+
export function verifyLedger(ledger) {
|
|
58
|
+
let prev = null;
|
|
59
|
+
const entries = ledger?.entries || [];
|
|
60
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
61
|
+
const e = entries[i];
|
|
62
|
+
const expected = sha256(JSON.stringify(payloadOf(i, e, prev)));
|
|
63
|
+
if (e.seq !== i) return { valid: false, brokenAt: i, reason: `entry ${i} has seq ${e.seq}` };
|
|
64
|
+
if (e.prev !== prev) return { valid: false, brokenAt: i, reason: `entry ${i} prev-link does not match` };
|
|
65
|
+
if (e.hash !== expected) return { valid: false, brokenAt: i, reason: `entry ${i} hash does not match its content (tampered)` };
|
|
66
|
+
prev = e.hash;
|
|
67
|
+
}
|
|
68
|
+
if (entries.length && ledger.head !== prev) return { valid: false, brokenAt: entries.length - 1, reason: 'head does not match the last entry' };
|
|
69
|
+
return { valid: true, brokenAt: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Typed convenience recorders , the common facts a project preserves ────────
|
|
73
|
+
const rec = (type) => (ledger, subject, data = {}, meta = {}) => record(ledger, { type, subject, ...meta, data });
|
|
74
|
+
export const recordIntentVersion = rec('intent-version'); // data: { hash, version }
|
|
75
|
+
export const recordDecision = rec('decision'); // note: the rationale ("why")
|
|
76
|
+
export const recordAssumption = rec('assumption');
|
|
77
|
+
export const recordApproval = rec('approval'); // actor: approver
|
|
78
|
+
export const recordRejection = rec('rejection');
|
|
79
|
+
export const recordCorrection = rec('correction'); // data: { from, to } , inferred intent a human fixed
|
|
80
|
+
export const recordRiskAcceptance = rec('risk-acceptance'); // data: { finding, reason, expires }
|
|
81
|
+
export const recordVerification = rec('verification-result'); // data: { passed, evidence }
|
|
82
|
+
export const recordFinding = rec('finding');
|
|
83
|
+
export const recordLessonVersion = rec('lesson-version'); // data: { version, stale }
|
|
84
|
+
|
|
85
|
+
// ── Queries , the Ledger answers ─────────────────────────────────────────────
|
|
86
|
+
const of = (ledger) => ledger?.entries || [];
|
|
87
|
+
export const history = (ledger, subject) => of(ledger).filter((e) => !subject || e.subject === subject);
|
|
88
|
+
export const whyBuilt = (ledger, subject) => history(ledger, subject).filter((e) => e.type === 'decision');
|
|
89
|
+
export const approvalsFor = (ledger, subject) => history(ledger, subject).filter((e) => e.type === 'approval' || e.type === 'rejection');
|
|
90
|
+
export const assumptionsFor = (ledger, subject) => history(ledger, subject).filter((e) => e.type === 'assumption');
|
|
91
|
+
export const correctionsFor = (ledger, subject) => history(ledger, subject).filter((e) => e.type === 'correction');
|
|
92
|
+
export const acceptedRisks = (ledger) => of(ledger).filter((e) => e.type === 'risk-acceptance');
|
|
93
|
+
export const staleLessons = (ledger) => of(ledger).filter((e) => e.type === 'lesson-version' && e.data?.stale);
|
|
94
|
+
export const verificationsFor = (ledger, subject) => history(ledger, subject).filter((e) => e.type === 'verification-result');
|
|
95
|
+
|
|
96
|
+
/** A structured answer to the nine Ledger questions for one subject (mission). */
|
|
97
|
+
export function explain(ledger, subject) {
|
|
98
|
+
return {
|
|
99
|
+
schema: LEDGER_SCHEMA,
|
|
100
|
+
subject,
|
|
101
|
+
why: whyBuilt(ledger, subject).map((e) => e.note || e.data?.reason).filter(Boolean),
|
|
102
|
+
approvedBy: approvalsFor(ledger, subject).filter((e) => e.type === 'approval').map((e) => e.actor).filter(Boolean),
|
|
103
|
+
assumptions: assumptionsFor(ledger, subject).map((e) => e.note || e.data).filter(Boolean),
|
|
104
|
+
verifications: verificationsFor(ledger, subject).map((e) => e.data),
|
|
105
|
+
corrections: correctionsFor(ledger, subject).map((e) => e.data),
|
|
106
|
+
acceptedRisks: acceptedRisks(ledger).filter((e) => !subject || e.subject === subject).map((e) => e.data),
|
|
107
|
+
changeCount: history(ledger, subject).length,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Temporal + lifecycle semantics (Gap 2). IL owns the formal state-machine IR and the
|
|
2
|
+
// STATIC model analysis (well-formedness, reachability, dead-ends) on the DECLARED
|
|
3
|
+
// lifecycle. OpenThunder verifies the IMPLEMENTED reality against this IR and produces
|
|
4
|
+
// counterexample traces. Pure (no Node deps): browser-safe. Deterministic.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the formal IR for one lifecycle: states, terminals, the initial state (the one
|
|
8
|
+
* with no inbound transition), the adjacency map, and the reachable set from initial.
|
|
9
|
+
*/
|
|
10
|
+
export function buildLifecycle(lc) {
|
|
11
|
+
const states = [...new Set(lc.states || [])];
|
|
12
|
+
const terminals = [...new Set(lc.terminals || [])];
|
|
13
|
+
const transitions = lc.transitions || [];
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const s of states) out[s] = [];
|
|
16
|
+
for (const t of transitions) if (out[t.from]) out[t.from].push(t.to);
|
|
17
|
+
// initial = the first declared state (the canonical start). The earlier "first state with no
|
|
18
|
+
// inbound transition" heuristic was wrong whenever the start state has a back-edge: in a cycle
|
|
19
|
+
// (e.g. A -> B -> A) every cyclic state has inbound, so it wrongly picked an isolated terminal.
|
|
20
|
+
const initial = states[0] ?? null;
|
|
21
|
+
// reachability (DFS from initial).
|
|
22
|
+
const reachable = new Set();
|
|
23
|
+
const stack = initial ? [initial] : [];
|
|
24
|
+
while (stack.length) {
|
|
25
|
+
const s = stack.pop();
|
|
26
|
+
if (reachable.has(s)) continue;
|
|
27
|
+
reachable.add(s);
|
|
28
|
+
for (const n of out[s] || []) stack.push(n);
|
|
29
|
+
}
|
|
30
|
+
return { name: lc.name, states, terminals, initial, transitions, out, reachable: [...reachable].sort() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Static analysis of a declared lifecycle. Returns { ir, findings } (deterministic). */
|
|
34
|
+
export function analyzeLifecycle(lc) {
|
|
35
|
+
const ir = buildLifecycle(lc);
|
|
36
|
+
const stateSet = new Set(ir.states);
|
|
37
|
+
const reachable = new Set(ir.reachable);
|
|
38
|
+
const findings = [];
|
|
39
|
+
|
|
40
|
+
for (const t of lc.transitions || []) {
|
|
41
|
+
for (const [side, s] of [['from', t.from], ['to', t.to]]) {
|
|
42
|
+
if (s && !stateSet.has(s)) findings.push({ code: 'IL-LIFE-001', message: `Transition "${t.name || '(unnamed)'}" ${side} references undefined state "${s}".` });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const term of ir.terminals) {
|
|
46
|
+
if (!stateSet.has(term)) findings.push({ code: 'IL-LIFE-001', message: `Terminal "${term}" is not a declared state.` });
|
|
47
|
+
else if ((ir.out[term] || []).length) findings.push({ code: 'IL-LIFE-002', message: `Terminal state "${term}" has an outgoing transition.` });
|
|
48
|
+
}
|
|
49
|
+
for (const s of ir.states) {
|
|
50
|
+
if (!reachable.has(s)) findings.push({ code: 'IL-LIFE-003', message: `State "${s}" is unreachable from the initial state.` });
|
|
51
|
+
else if (!ir.terminals.includes(s) && (ir.out[s] || []).length === 0) {
|
|
52
|
+
findings.push({ code: 'IL-LIFE-004', message: `Non-terminal state "${s}" has no outgoing transition (dead end).` });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { ir, findings };
|
|
56
|
+
}
|