diff --git a/examples/infospace-with-history/infospace.yaml b/examples/infospace-with-history/infospace.yaml index 882931f6..0795190b 100644 --- a/examples/infospace-with-history/infospace.yaml +++ b/examples/infospace-with-history/infospace.yaml @@ -17,6 +17,7 @@ schemas: entity: schemas/economic-entity-schema-v1.0.md mapping: schemas/vsm-mapping-schema-v1.0.md analysis: schemas/chapter-analysis-schema-v1.0.md + relation: schemas/relation-schema-v1.0.md competency_questions: | 1. How does Smith's division of labour map to VSM System 1 operations? diff --git a/examples/infospace-with-history/output/relations/accumulation_of_stock--enables--division_of_labour.md b/examples/infospace-with-history/output/relations/accumulation_of_stock--enables--division_of_labour.md new file mode 100644 index 00000000..15e431de --- /dev/null +++ b/examples/infospace-with-history/output/relations/accumulation_of_stock--enables--division_of_labour.md @@ -0,0 +1,38 @@ +# Accumulation of Stock — enables — Division of Labour + +## Subject + +Accumulation of Stock + +## Predicate + +enables + +## Object + +Division of Labour + +## Relation Type + +enables + +## VSM Channel + +S3 → S1 (capital stock managed at S3 deploys productive capacity at S1) + +## Evidence + +Book I, Chapter 6: "As soon as stock has accumulated in the hands of +particular persons, some of them will naturally employ it in setting to work +industrious people, whom they will supply with materials and subsistence." + +## Feedback Role + +**Capital Accumulation loop (positive/reinforcing):** +Accumulation of Stock → enables → Division of Labour → increases → +Productive Powers of Labour → raises → Profits of Stock → funds → +Accumulation of Stock. + +This edge (enables) is the initiating link: accumulated capital is the +prerequisite for organising specialised production. Without prior accumulation +there is no employer to assign and coordinate different tasks. diff --git a/examples/infospace-with-history/output/relations/accumulation_of_stock--increases--market_price_of_commodities.md b/examples/infospace-with-history/output/relations/accumulation_of_stock--increases--market_price_of_commodities.md new file mode 100644 index 00000000..95442bf9 --- /dev/null +++ b/examples/infospace-with-history/output/relations/accumulation_of_stock--increases--market_price_of_commodities.md @@ -0,0 +1,42 @@ +# Accumulation of Stock — increases supply that depresses — Market Price of Commodities + +## Subject + +Accumulation of Stock + +## Predicate + +increases supply, depressing + +## Object + +Market Price of Commodities + +## Relation Type + +regulates + +## VSM Channel + +S3 → S2 (capital deployment increases supply, correcting price signal) + +## Evidence + +Book I, Chapter 7: "When the quantity brought to market exceeds the +effectual demand, it cannot be all sold to those who are willing to pay +the whole value of the rent, wages and profit, which must be paid in +order to bring it thither. Some part must be sold to those who are +willing to pay less, and the low price which they give for it must +reduce the price of the whole." + +## Feedback Role + +**Market Price Balancing loop (negative/balancing):** +Market Price above Natural Price → attracts → Capital inflow → +increases supply → depresses → Market Price back toward Natural Price. + +This edge (increases supply, depressing) closes the balancing loop: +directed capital raises output until oversupply brings price back down. +This is the mechanism behind Smith's claim that natural price acts as +the "central price, to which the prices of all commodities are +continually gravitating." diff --git a/examples/infospace-with-history/output/relations/division_of_labour--constrains--market_extent.md b/examples/infospace-with-history/output/relations/division_of_labour--constrains--market_extent.md new file mode 100644 index 00000000..f190077c --- /dev/null +++ b/examples/infospace-with-history/output/relations/division_of_labour--constrains--market_extent.md @@ -0,0 +1,40 @@ +# Division of Labour — constrains — Market Extent + +## Subject + +Division of Labour + +## Predicate + +limited by + +## Object + +Market Extent + +## Relation Type + +constrains + +## VSM Channel + +S2 ← S1 (the coordination reach of markets bounds the scope of operational specialisation) + +## Evidence + +Book I, Chapter 3: "As it is the power of exchanging that gives occasion +to the division of labour, so the extent of this division must always be +limited by the extent of that power, or, in other words, by the extent +of the market." + +## Feedback Role + +**Market Expansion loop (positive/reinforcing):** +Market Extent → enables greater Division of Labour → raises Productive +Powers of Labour → generates surplus → expands Trade → expands +Market Extent. + +This edge (limited by) is the structural constraint that gives the loop +its character: the level of specialisation that can be sustained depends +on the size of the market that can absorb the specialised output. +Smith's famous opening of Chapter III states this with unusual directness. diff --git a/examples/infospace-with-history/output/relations/division_of_labour--increases--productive_powers_of_labour.md b/examples/infospace-with-history/output/relations/division_of_labour--increases--productive_powers_of_labour.md new file mode 100644 index 00000000..97787ade --- /dev/null +++ b/examples/infospace-with-history/output/relations/division_of_labour--increases--productive_powers_of_labour.md @@ -0,0 +1,40 @@ +# Division of Labour — increases — Productive Powers of Labour + +## Subject + +Division of Labour + +## Predicate + +increases + +## Object + +Productive Powers of Labour + +## Relation Type + +enables + +## VSM Channel + +S1 → S1 (operational specialisation raises operational output per unit) + +## Evidence + +Book I, Chapter 1: "The greatest improvement in the productive powers of +labour, and the greater part of the skill, dexterity, and judgment with +which it is anywhere directed, or applied, seem to have been the effects +of the division of labour." + +## Feedback Role + +**Capital Accumulation loop (positive/reinforcing):** +Accumulation of Stock → enables → Division of Labour → increases → +Productive Powers of Labour → raises → Profits of Stock → funds → +Accumulation of Stock. + +This edge (increases) is the productivity mechanism: specialisation +improves dexterity, saves time between tasks, and facilitates the +invention of machinery — three distinct channels that all raise output +per worker. diff --git a/examples/infospace-with-history/output/relations/invisible_hand_mechanism--coordinates--accumulation_of_stock.md b/examples/infospace-with-history/output/relations/invisible_hand_mechanism--coordinates--accumulation_of_stock.md new file mode 100644 index 00000000..bc63c13b --- /dev/null +++ b/examples/infospace-with-history/output/relations/invisible_hand_mechanism--coordinates--accumulation_of_stock.md @@ -0,0 +1,40 @@ +# Invisible Hand Mechanism — coordinates — Accumulation of Stock + +## Subject + +Invisible Hand Mechanism + +## Predicate + +coordinates (directs capital allocation without central direction) + +## Object + +Accumulation of Stock + +## Relation Type + +coordinates + +## VSM Channel + +S4 → S3 (distributed environmental intelligence shapes capital allocation decisions) + +## Evidence + +Book IV, Chapter 2: "He generally, indeed, neither intends to promote the +public interest, nor knows how much he is promoting it... he intends only +his own security; and by directing that industry in a manner whose produce +may be of the greatest value, he intends only his own gain, and he is in +this, as in many other cases, led by an invisible hand to promote an end +which was no part of his intention." + +## Feedback Role + +The Invisible Hand Mechanism is the S4 intelligence function that makes +the Market Price Balancing loop self-organising. Individual capitalists +seeking private profit collectively direct accumulation to where returns +are highest, which (through the market price balancing loop) also serves +as a resource allocation mechanism. This relation explains why the system +does not require a planner — the S4 function is distributed across all +capital owners simultaneously. diff --git a/examples/infospace-with-history/output/relations/market_extent--enables--division_of_labour.md b/examples/infospace-with-history/output/relations/market_extent--enables--division_of_labour.md new file mode 100644 index 00000000..ff27bdae --- /dev/null +++ b/examples/infospace-with-history/output/relations/market_extent--enables--division_of_labour.md @@ -0,0 +1,40 @@ +# Market Extent — enables — Division of Labour + +## Subject + +Market Extent + +## Predicate + +enables (by providing demand for specialised output) + +## Object + +Division of Labour + +## Relation Type + +enables + +## VSM Channel + +S2 → S1 (coordination reach of markets enables operational specialisation) + +## Evidence + +Book I, Chapter 3: "When the market is very small, no person can have +any encouragement to dedicate himself entirely to one employment, for +want of the power to exchange all that surplus part of the produce of his +own labour, which is over and above his own consumption, for such parts +of the produce of other men's labour as he has occasion for." + +## Feedback Role + +**Market Expansion loop (positive/reinforcing):** +Market Extent → enables → Division of Labour → raises Productive Powers +of Labour → generates surplus → expands Trade → expands Market Extent. + +Together with the constrains edge (Division of Labour ← Market Extent), +this pair forms the mutual dependency at the heart of Book I: markets +enable specialisation, and specialisation generates the surplus that +expands markets. diff --git a/examples/infospace-with-history/output/relations/market_price_of_commodities--attracts--accumulation_of_stock.md b/examples/infospace-with-history/output/relations/market_price_of_commodities--attracts--accumulation_of_stock.md new file mode 100644 index 00000000..5ab80cf1 --- /dev/null +++ b/examples/infospace-with-history/output/relations/market_price_of_commodities--attracts--accumulation_of_stock.md @@ -0,0 +1,41 @@ +# Market Price of Commodities — attracts — Accumulation of Stock + +## Subject + +Market Price of Commodities + +## Predicate + +attracts (when above natural price) + +## Object + +Accumulation of Stock + +## Relation Type + +coordinates + +## VSM Channel + +S2 → S3 (price signal coordinates capital reallocation) + +## Evidence + +Book I, Chapter 7: "When the quantity brought to market falls short of the +effectual demand, all those who are willing to pay the whole natural price +cannot be supplied. Some of them will be willing to give more. A competition +will immediately begin among them, and the market price will rise more or +less above the natural price." + +## Feedback Role + +**Market Price Balancing loop (negative/balancing):** +Market Price above Natural Price → attracts → Capital inflow (Accumulation +directed to this market) → increases Supply → depresses → Market Price +back toward Natural Price. + +This edge (attracts) is the signal-response link: a market price above +natural price is a profit opportunity that draws capital from other uses. +Smith's key insight is that this signal is self-cancelling — it eliminates +the very excess that created it. diff --git a/examples/infospace-with-history/output/relations/market_price_of_commodities--decomposes_into--profits_of_stock.md b/examples/infospace-with-history/output/relations/market_price_of_commodities--decomposes_into--profits_of_stock.md new file mode 100644 index 00000000..ce5192a0 --- /dev/null +++ b/examples/infospace-with-history/output/relations/market_price_of_commodities--decomposes_into--profits_of_stock.md @@ -0,0 +1,37 @@ +# Market Price of Commodities — decomposes into — Profits of Stock + +## Subject + +Market Price of Commodities + +## Predicate + +decomposes into (profit as component) + +## Object + +Profits of Stock + +## Relation Type + +produces + +## VSM Channel + +S2 → S3 (price signal resolves into return on capital, which S3 manages) + +## Evidence + +Book I, Chapter 6: "In every society the price of every commodity +finally resolves itself into some one or other, or all of those three +parts; and in every improved society, all three enter more or less, +as component parts, into the price of the far greater part of commodities." + +## Feedback Role + +Part of the **Three-Component Decomposition** (see also wages and rent +relations). This edge links the market price coordination signal (S2) +to the capital management outcome (S3), establishing profit as the +mechanism by which market performance translates into the fund for +future accumulation. It is the bridge between the Market Price Balancing +loop and the Capital Accumulation loop. diff --git a/examples/infospace-with-history/output/relations/market_price_of_commodities--decomposes_into--wages_of_labour.md b/examples/infospace-with-history/output/relations/market_price_of_commodities--decomposes_into--wages_of_labour.md new file mode 100644 index 00000000..b39e55ce --- /dev/null +++ b/examples/infospace-with-history/output/relations/market_price_of_commodities--decomposes_into--wages_of_labour.md @@ -0,0 +1,38 @@ +# Market Price of Commodities — decomposes into — Wages of Labour + +## Subject + +Market Price of Commodities + +## Predicate + +decomposes into (wages as component) + +## Object + +Wages of Labour + +## Relation Type + +produces + +## VSM Channel + +S2 → S2 (price as coordination signal resolves into its component coordination signals) + +## Evidence + +Book I, Chapter 6: "In every society the price of every commodity +finally resolves itself into some one or other, or all of those three +parts [wages, profit, rent]; and in every improved society, all three +enter more or less, as component parts, into the price of the far greater +part of commodities." + +## Feedback Role + +This is one of the three price-decomposition relations (alongside market +price → profits of stock, and market price → rent of land). Together +they form the **Three-Component Decomposition**: the price system +connects the coordination layer (S2) back to the distributional outcomes +for all three factor markets. The decomposition is the analytical +foundation from which Books I and II derive their structure. diff --git a/examples/infospace-with-history/output/relations/natural_price_as_central_price--centres--market_price_of_commodities.md b/examples/infospace-with-history/output/relations/natural_price_as_central_price--centres--market_price_of_commodities.md new file mode 100644 index 00000000..193db01c --- /dev/null +++ b/examples/infospace-with-history/output/relations/natural_price_as_central_price--centres--market_price_of_commodities.md @@ -0,0 +1,35 @@ +# Natural Price as Central Price — centres — Market Price of Commodities + +## Subject + +Natural Price as Central Price + +## Predicate + +centres + +## Object + +Market Price of Commodities + +## Relation Type + +coordinates + +## VSM Channel + +S2 (natural price is the attractor in the S2 coordination field) + +## Evidence + +Book I, Chapter 7: "The natural price, therefore, is, as it were, the +central price, to which the prices of all commodities are continually +gravitating. Different accidents may sometimes keep them suspended a good +deal above it, and sometimes force them down even somewhat below it." + +## Feedback Role + +This relation is the attractor definition for the Market Price Balancing +loop — natural price is the equilibrium that both loops (attraction of +capital when above, flight of capital when below) converge on. It is the +S2 coordination reference point around which oscillation is suppressed. diff --git a/examples/infospace-with-history/output/relations/productive_powers_of_labour--raises--profits_of_stock.md b/examples/infospace-with-history/output/relations/productive_powers_of_labour--raises--profits_of_stock.md new file mode 100644 index 00000000..6fde4e61 --- /dev/null +++ b/examples/infospace-with-history/output/relations/productive_powers_of_labour--raises--profits_of_stock.md @@ -0,0 +1,39 @@ +# Productive Powers of Labour — raises — Profits of Stock + +## Subject + +Productive Powers of Labour + +## Predicate + +raises + +## Object + +Profits of Stock + +## Relation Type + +produces + +## VSM Channel + +S1 → S3 (operational productivity surplus allocated as profit at capital management level) + +## Evidence + +Book I, Chapter 6: "The value which the workmen add to the materials +resolves itself into two parts, of which the one pays their wages, the +other the profits of their employer upon the whole stock of materials +and wages which he advanced." + +## Feedback Role + +**Capital Accumulation loop (positive/reinforcing):** +Accumulation of Stock → enables → Division of Labour → increases → +Productive Powers of Labour → raises → Profits of Stock → funds → +Accumulation of Stock. + +This edge (raises) is the surplus-extraction link: the productivity gain +produced by specialisation shows up as profit over and above wage costs, +generating the fund from which further accumulation occurs. diff --git a/examples/infospace-with-history/output/relations/profits_of_stock--funds--accumulation_of_stock.md b/examples/infospace-with-history/output/relations/profits_of_stock--funds--accumulation_of_stock.md new file mode 100644 index 00000000..4556a5b0 --- /dev/null +++ b/examples/infospace-with-history/output/relations/profits_of_stock--funds--accumulation_of_stock.md @@ -0,0 +1,40 @@ +# Profits of Stock — funds — Accumulation of Stock + +## Subject + +Profits of Stock + +## Predicate + +funds + +## Object + +Accumulation of Stock + +## Relation Type + +produces + +## VSM Channel + +S3 → S3 (profit retained by capital managers re-enters the stock managed at S3) + +## Evidence + +Book II, Chapter 3: "Whatever a person saves from his revenue he adds to +his capital, and either employs it himself in maintaining an additional +number of productive hands, or enables some other person to do so, by +lending it to him for an interest." + +## Feedback Role + +**Capital Accumulation loop (positive/reinforcing):** +Accumulation of Stock → enables → Division of Labour → increases → +Productive Powers of Labour → raises → Profits of Stock → funds → +Accumulation of Stock. + +This edge (funds) closes the loop: profit not consumed becomes saving, +and saving becomes the new stock that enables the next round of division +of labour. This is why Smith regards frugality as a public virtue — it +fuels productive capacity rather than dissipating it. diff --git a/examples/infospace-with-history/output/relations/rent_of_land--regulated_by--market_price_of_commodities.md b/examples/infospace-with-history/output/relations/rent_of_land--regulated_by--market_price_of_commodities.md new file mode 100644 index 00000000..1e1d38a6 --- /dev/null +++ b/examples/infospace-with-history/output/relations/rent_of_land--regulated_by--market_price_of_commodities.md @@ -0,0 +1,37 @@ +# Rent of Land — regulated by — Market Price of Commodities + +## Subject + +Rent of Land + +## Predicate + +regulated by (rent is a residual after wages and profit are paid) + +## Object + +Market Price of Commodities + +## Relation Type + +is regulated by + +## VSM Channel + +S2 → S3 (price signal determines what residual rent the land can command) + +## Evidence + +Book I, Chapter 11: "Rent, it is to be observed, therefore, enters into +the composition of the price of commodities in a different manner from +wages and profit. High or low wages and profit are the causes of high or +low price; high or low rent is the effect of it." + +## Feedback Role + +This relation establishes rent as a **residual** in the price decomposition +rather than a cost that determines price. It places Rent of Land in a +structurally subordinate position to the Market Price mechanism — rent +takes what is left after labour and capital have been paid, rather than +competing with them as a co-equal cost component. This is Smith's +sharpest break from the physiocrats. diff --git a/examples/infospace-with-history/output/relations/wages_of_labour--regulated_by--accumulation_of_stock.md b/examples/infospace-with-history/output/relations/wages_of_labour--regulated_by--accumulation_of_stock.md new file mode 100644 index 00000000..4e354b97 --- /dev/null +++ b/examples/infospace-with-history/output/relations/wages_of_labour--regulated_by--accumulation_of_stock.md @@ -0,0 +1,39 @@ +# Wages of Labour — regulated by — Accumulation of Stock + +## Subject + +Wages of Labour + +## Predicate + +regulated by (set by the demand for labour, which tracks capital growth) + +## Object + +Accumulation of Stock + +## Relation Type + +is regulated by + +## VSM Channel + +S3 → S2 (capital stock sets demand for labour, which sets the wage coordination signal) + +## Evidence + +Book I, Chapter 8: "The demand for those who live by wages, therefore, +necessarily increases with the increase of the revenue and stock of every +country, and cannot possibly increase without it. The increase of revenue +and stock is the increase of national wealth." + +## Feedback Role + +**Wages Expansion loop (positive/reinforcing):** +Accumulation of Stock → raises demand for labour → raises Wages of Labour +→ raises consumer capacity → stimulates Productive Powers of Labour → +generates surplus → funds Accumulation of Stock. + +This edge (regulated by) establishes the first link: the wage rate +responds to the growth of capital stock, not to the current stock level +directly. Rapid accumulation raises wages; stationary stock does not. diff --git a/examples/infospace-with-history/output/relations/wages_of_labour--regulated_by--profits_of_stock.md b/examples/infospace-with-history/output/relations/wages_of_labour--regulated_by--profits_of_stock.md new file mode 100644 index 00000000..174fe925 --- /dev/null +++ b/examples/infospace-with-history/output/relations/wages_of_labour--regulated_by--profits_of_stock.md @@ -0,0 +1,39 @@ +# Wages of Labour — regulated by — Profits of Stock + +## Subject + +Wages of Labour + +## Predicate + +regulated by (bounded above by) + +## Object + +Profits of Stock + +## Relation Type + +is regulated by + +## VSM Channel + +S3 → S2 (capital management sets the upper bound on the coordination signal) + +## Evidence + +Book I, Chapter 8: "In order to bring up a family, the labour of the +husband and wife together must, even in the lowest species of common +labour, be able to earn something more than what is precisely necessary +for their own maintenance; but in what is commonly called common labour +it is rare that a single labourer earns much more... The interests of +the two parties are by no means the same. The workmen desire to get as +much, the masters to give as little as possible." + +## Feedback Role + +This relation establishes the distributional constraint that bounds +the Wages loop. Profits of Stock and Wages of Labour are in permanent +tension: every increase in one (holding productivity constant) reduces +the other. This is Smith's clearest statement of class interest as a +structural feature of market economies. diff --git a/examples/infospace-with-history/schemas/relation-schema-v1.0.md b/examples/infospace-with-history/schemas/relation-schema-v1.0.md new file mode 100644 index 00000000..68c8998a --- /dev/null +++ b/examples/infospace-with-history/schemas/relation-schema-v1.0.md @@ -0,0 +1,139 @@ +# Relation Triplet Schema — v1.0 + +Each file in `output/relations/` captures a single directed relation between +two entities from the L1 entity collection. Relations are the edges of the +L3 relation graph. + +--- + +## Required Sections + +### H1 — Relation title + +Format: `# Subject — predicate phrase — Object` + +Example: `# Division of Labour — limited by — Market Extent` + +--- + +### Subject + +The entity that is the **source** of the relation — the thing doing the +enabling, constraining, producing, etc. + +Use the entity's exact title as it appears in its L1 entity file (H1 heading). + +--- + +### Predicate + +A short phrase from the **controlled relation vocabulary** (see below), or +a natural language variant that maps to one of those classes. + +Examples: `limited by`, `enables`, `is regulated by`, `produces` + +--- + +### Object + +The entity that is the **target** of the relation — the thing being enabled, +constrained, regulated, etc. + +Use the entity's exact title as it appears in its L1 entity file. + +--- + +### Relation Type + +The **semantic class** of the predicate, drawn from the controlled vocabulary: + +| Relation Type | Meaning | Default VSM Channel | +|---|---|---| +| `enables` | Subject makes object possible or more effective | S1 → S1 | +| `constrains` | Subject limits or bounds the object | S1 ← S1 | +| `regulates` | Subject actively governs the object | S3 → S1 | +| `is regulated by` | Object governs the subject | S1 ← S3 | +| `coordinates` | Subject reduces oscillation or misalignment | S2 | +| `produces` | Subject generates the object as output | S1 | +| `consumes` | Subject depletes the object | S1 | +| `monitors` | Subject observes and reports on object | S3* | +| `audits` | Subject verifies compliance of object | S3* | +| `adapts to` | Subject adjusts in response to object | S4 | +| `anticipates` | Subject predicts or models the object | S4 | +| `defines` | Subject sets the identity or rules of object | S5 → any | +| `is defined by` | Object sets the identity or rules of subject | any ← S5 | +| `contradicts` | Subject is in direct logical conflict with object | any | +| `tensions with` | Subject and object exist in productive tension | any | + +--- + +### VSM Channel + +The Viable System Model systems involved in this relation, with direction. + +Format: `S → S` or `S` if the relation is within a single system. + +Examples: +- `S1 → S2` — operation drives a coordination signal +- `S3 → S1` — management regulates operations +- `S2` — pure coordination within S2 +- `S4 → S5` — intelligence informing policy + +Use `S3*` for audit loops (Beer's algedonic channel). + +--- + +### Evidence + +Direct textual evidence from Adam Smith's text. Preferred format: + +``` +Book I, Chapter 3: "The division of labour is limited by the extent of the market." +``` + +Use short quotes when available. Chapter reference alone is acceptable when +no single sentence captures the relation. + +--- + +### Feedback Role (optional) + +If this relation is part of a named feedback loop, describe its role here. +Name the loop and state what the relation contributes. + +Example: +``` +Part of the Market Expansion loop: larger market → more specialisation +→ higher productivity → greater surplus → expanded trade → larger market. +This edge (limited by) is the constraining link that turns the loop balancing +when market extent cannot grow further. +``` + +Omit or leave empty if the relation is not part of a known feedback loop. + +--- + +## File Naming + +Use the pattern: `----.md` + +Where slugs are derived from entity titles using the same slugification as +entity files (lowercase, spaces and punctuation replaced with underscores). + +Example: `division_of_labour--constrains--market_extent.md` + +If two entities have the same relation type between them (unusual), append +a numeric suffix: `...-2.md`. + +--- + +## Completeness Criteria + +A relation file is **complete** if: +- All four required sections are present and non-empty +- The Relation Type is from the controlled vocabulary +- The VSM Channel is a valid VSM system designation +- Evidence references a specific Book and Chapter + +A relation file is **acceptable** if: +- Required sections present but Evidence is missing (mark as `(unverified)`) diff --git a/markitect/infospace/cli.py b/markitect/infospace/cli.py index 6b3f25a0..9796209a 100644 --- a/markitect/infospace/cli.py +++ b/markitect/infospace/cli.py @@ -299,6 +299,126 @@ def eval_summary(config_path: Optional[str], update_metrics: bool): click.echo(f"\nUpdated metrics.yaml: per_entity_mean = {mean_overall:.4f}") +# ── relations ───────────────────────────────────────────────────────── + + +@infospace_commands.command() +@click.option("--config", "config_path", default=None, help="Path to infospace.yaml.") +@click.option("--entity", "entity_slug", default=None, + help="Show only relations involving this entity slug.") +@click.option("--vsm", "vsm_filter", default=None, + help="Show only relations whose VSM channel contains this string (e.g. S2, S3).") +@click.option("--loops", "loops_only", is_flag=True, default=False, + help="Show only feedback loops (cycles in the relation graph).") +@click.option("--stats", "stats_only", is_flag=True, default=False, + help="Show aggregate statistics only, no individual relations.") +def relations(config_path: Optional[str], entity_slug: Optional[str], + vsm_filter: Optional[str], loops_only: bool, stats_only: bool): + """Show the L3 relation graph — triplets, feedback loops, and VSM channels.""" + cfg, cfg_path = _load_config_or_exit(config_path) + root = cfg_path.parent + + from markitect.infospace.relation_parser import parse_relations_directory + + relations_dir = root / cfg.relations_dir + if not relations_dir.is_dir(): + click.echo("No relations directory found. Create output/relations/ and add relation files.") + return + + all_relations = parse_relations_directory(relations_dir) + if not all_relations: + click.echo("No relation files found in " + str(relations_dir)) + return + + # Build directed graph for cycle detection + try: + import networkx as nx + G = nx.DiGraph() + for r in all_relations: + G.add_edge(r.subject_slug, r.object_slug, + predicate=r.predicate, + relation_type=r.relation_type, + vsm_channel=r.vsm_channel, + slug=r.slug) + except ImportError: + G = None + + # Find feedback loops + loops = [] + if G is not None: + try: + loops = list(nx.simple_cycles(G)) + except Exception: + loops = [] + + # Stats summary + import re as _re + + def _vsm_code(channel: str) -> str: + """Strip parenthetical description, returning just the system code (e.g. 'S3 → S1').""" + return _re.sub(r'\s*\(.*', '', channel).strip() or channel + + n = len(all_relations) + vsm_counts: dict = {} + type_counts: dict = {} + for r in all_relations: + vsm_counts[_vsm_code(r.vsm_channel)] = vsm_counts.get(_vsm_code(r.vsm_channel), 0) + 1 + type_counts[r.relation_type] = type_counts.get(r.relation_type, 0) + 1 + + click.echo(f"Relation graph — {n} relations") + if G is not None: + click.echo(f" Entities in graph: {G.number_of_nodes()}") + click.echo(f" Feedback loops: {len(loops)}") + click.echo() + + if stats_only: + click.echo("Relation types:") + for rt, count in sorted(type_counts.items(), key=lambda x: -x[1]): + click.echo(f" {rt:<25} {count:>4}") + click.echo() + click.echo("VSM channels:") + for ch, count in sorted(vsm_counts.items(), key=lambda x: -x[1]): + click.echo(f" {ch:<20} {count:>4}") + return + + # Feedback loops section + if loops or loops_only: + if loops: + click.echo(f"Feedback loops ({len(loops)}):") + for i, cycle in enumerate(loops, 1): + click.echo(f" Loop {i}: {' → '.join(cycle)} → {cycle[0]}") + click.echo() + elif loops_only: + click.echo("No feedback loops detected in current relation set.") + return + + if loops_only: + return + + # Filter relations + filtered = all_relations + if entity_slug: + filtered = [r for r in filtered + if entity_slug in (r.subject_slug, r.object_slug)] + if not filtered: + click.echo(f"No relations found involving '{entity_slug}'.") + return + if vsm_filter: + filtered = [r for r in filtered if vsm_filter in r.vsm_channel] + if not filtered: + click.echo(f"No relations with VSM channel containing '{vsm_filter}'.") + return + + # Display relations + click.echo(f"{'Subject':<35} {'Predicate':<30} {'Object':<35} {'VSM'}") + click.echo("-" * 110) + for r in filtered: + subj = r.subject[:33] + ".." if len(r.subject) > 35 else r.subject + obj = r.object[:33] + ".." if len(r.object) > 35 else r.object + pred = r.predicate[:28] + ".." if len(r.predicate) > 30 else r.predicate + click.echo(f"{subj:<35} {pred:<30} {obj:<35} {r.vsm_channel}") + + # ── viability ──────────────────────────────────────────────────────── diff --git a/markitect/infospace/config.py b/markitect/infospace/config.py index 86245c15..167b1e07 100644 --- a/markitect/infospace/config.py +++ b/markitect/infospace/config.py @@ -254,6 +254,7 @@ class InfospaceConfig: entities_dir: str = "output/entities" evaluations_dir: str = "output/evaluations" metrics_dir: str = "output/metrics" + relations_dir: str = "output/relations" def to_dict(self) -> Dict[str, Any]: d: Dict[str, Any] = {"topic": self.topic.to_dict()} @@ -276,6 +277,8 @@ class InfospaceConfig: d["evaluations_dir"] = self.evaluations_dir if self.metrics_dir != "output/metrics": d["metrics_dir"] = self.metrics_dir + if self.relations_dir != "output/relations": + d["relations_dir"] = self.relations_dir return d @classmethod @@ -301,6 +304,7 @@ class InfospaceConfig: entities_dir=data.get("entities_dir", "output/entities"), evaluations_dir=data.get("evaluations_dir", "output/evaluations"), metrics_dir=data.get("metrics_dir", "output/metrics"), + relations_dir=data.get("relations_dir", "output/relations"), ) diff --git a/markitect/infospace/relation_models.py b/markitect/infospace/relation_models.py new file mode 100644 index 00000000..8964fa83 --- /dev/null +++ b/markitect/infospace/relation_models.py @@ -0,0 +1,72 @@ +""" +Data models for L3 relation triplets. + +A relation triplet is the fundamental unit of the relation graph: + Subject --[Predicate]--> Object + +Each triplet is stored as a markdown file in ``output/relations/``. +""" + +from dataclasses import dataclass, field +from typing import List, Optional + + +# Controlled relation vocabulary — maps semantic class to VSM channel +RELATION_TYPES = { + "enables": "S1 → S1", + "constrains": "S1 ← S1", + "regulates": "S3 → S1", + "is regulated by": "S1 ← S3", + "coordinates": "S2", + "produces": "S1", + "consumes": "S1", + "monitors": "S3*", + "audits": "S3*", + "adapts to": "S4", + "anticipates": "S4", + "defines": "S5 → any", + "is defined by": "any ← S5", + "contradicts": "any", + "tensions with": "any", +} + + +@dataclass +class RelationMeta: + """Structured metadata for a single relation triplet. + + Attributes: + slug: Unique identifier, e.g. + ``division_of_labour--constrains--market_extent`` + subject: Human-readable title of the subject entity + subject_slug: Slug of the subject entity (links to L1) + predicate: Human-readable predicate phrase, e.g. "limited by" + object: Human-readable title of the object entity + object_slug: Slug of the object entity (links to L1) + relation_type: Semantic class from the controlled vocabulary + vsm_channel: VSM systems involved, e.g. "S1 → S2" + evidence: Source text quote or chapter reference + feedback_role: Description of role in a feedback loop (if any) + source_path: Absolute path to the ``.md`` file + """ + + slug: str + subject: str + subject_slug: str + predicate: str + object: str + object_slug: str + relation_type: str + vsm_channel: str + evidence: str = "" + feedback_role: str = "" + source_path: str = "" + + @property + def is_feedback_member(self) -> bool: + """True if this relation participates in a named feedback loop.""" + return bool(self.feedback_role.strip()) + + def edge(self) -> tuple: + """Return a (subject_slug, object_slug, predicate) edge tuple.""" + return (self.subject_slug, self.object_slug, self.predicate) diff --git a/markitect/infospace/relation_parser.py b/markitect/infospace/relation_parser.py new file mode 100644 index 00000000..c0c6103b --- /dev/null +++ b/markitect/infospace/relation_parser.py @@ -0,0 +1,137 @@ +""" +Relation triplet parser. + +Reads structured :class:`RelationMeta` objects from relation markdown +files in ``output/relations/``. + +File format:: + + # Subject — predicate — Object + + ## Subject + Subject Entity Title + + ## Predicate + predicate phrase + + ## Object + Object Entity Title + + ## Relation Type + constrains + + ## VSM Channel + S1 → S2 + + ## Evidence + Book I, Chapter 3: "..." + + ## Feedback Role + Part of the Market Expansion loop: ... +""" + +from __future__ import annotations + +import logging +import re +from pathlib import Path +from typing import List, Optional, Sequence + +from markitect.core.parser import parse_markdown_to_ast +from markitect.core.section_tree import ( + build_section_tree, + extract_section_text, + slugify, +) +from .relation_models import RelationMeta + +logger = logging.getLogger(__name__) + + +def _find_h2(tree_root: dict, slug: str) -> Optional[dict]: + for child in tree_root.get("children", []): + if child["level"] == 2 and child["slug"] == slug: + return child + return None + + +def _section_text(root: dict, slug: str) -> str: + node = _find_h2(root, slug) + return extract_section_text(node).strip() if node else "" + + +def _slug_from_title(title: str) -> str: + """Convert entity title to slug (same as slugify used in entity_parser).""" + return slugify(title) + + +def parse_relation_file(path: Path) -> RelationMeta: + """Parse a single relation markdown file into :class:`RelationMeta`. + + Raises: + ValueError: If required sections are missing. + """ + content = path.read_text(encoding="utf-8") + tokens = parse_markdown_to_ast(content) + tree = build_section_tree(tokens) + + # Find H1 + h1 = next( + (c for c in tree["children"] if c["level"] == 1), + None, + ) + if h1 is None: + raise ValueError(f"No H1 heading in {path}") + + root = h1 + + subject = _section_text(root, "subject") + predicate = _section_text(root, "predicate") + obj = _section_text(root, "object") + relation_type = _section_text(root, "relation_type") + vsm_channel = _section_text(root, "vsm_channel") + evidence = _section_text(root, "evidence") + feedback_role = _section_text(root, "feedback_role") + + if not subject: + raise ValueError(f"Missing ## Subject in {path}") + if not predicate: + raise ValueError(f"Missing ## Predicate in {path}") + if not obj: + raise ValueError(f"Missing ## Object in {path}") + + subject_slug = _slug_from_title(subject) + object_slug = _slug_from_title(obj) + + # Derive canonical slug from file stem + slug = path.stem + + return RelationMeta( + slug=slug, + subject=subject, + subject_slug=subject_slug, + predicate=predicate, + object=obj, + object_slug=object_slug, + relation_type=relation_type, + vsm_channel=vsm_channel, + evidence=evidence, + feedback_role=feedback_role, + source_path=str(path), + ) + + +def parse_relations_directory( + directory: Path, +) -> List[RelationMeta]: + """Parse all relation files in *directory*. + + Malformed files are skipped with a warning. + """ + relations: List[RelationMeta] = [] + for md_file in sorted(directory.glob("*.md")): + try: + relations.append(parse_relation_file(md_file)) + except Exception as exc: + logger.warning("Skipping relation file %s: %s", md_file.name, exc) + return relations